This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new 64ee4fab55 Improve HTTP If headers processing according to RFC 9110
64ee4fab55 is described below
commit 64ee4fab55bb850d790fde45ce92d9cd5f6a7663
Author: remm <[email protected]>
AuthorDate: Wed Dec 11 10:56:06 2024 +0100
Improve HTTP If headers processing according to RFC 9110
PR#796 by Chenjp.
Also includes better test cases.
---
.../apache/catalina/servlets/DefaultServlet.java | 376 +++++++++---
.../TestDefaultServletRfc9110Section13.java | 672 +++++++++++++++++++++
...efaultServletRfc9110Section13Parameterized.java | 433 +++++++++++++
webapps/docs/changelog.xml | 4 +
4 files changed, 1387 insertions(+), 98 deletions(-)
diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java
b/java/org/apache/catalina/servlets/DefaultServlet.java
index 2accac435e..a8cb7525d0 100644
--- a/java/org/apache/catalina/servlets/DefaultServlet.java
+++ b/java/org/apache/catalina/servlets/DefaultServlet.java
@@ -749,10 +749,72 @@ public class DefaultServlet extends HttpServlet {
*/
protected boolean checkIfHeaders(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
throws IOException {
+ String ifNoneMatchHeader = request.getHeader("If-None-Match");
- return checkIfMatch(request, response, resource) &&
checkIfModifiedSince(request, response, resource) &&
- checkIfNoneMatch(request, response, resource) &&
checkIfUnmodifiedSince(request, response, resource);
-
+ // RFC9110 #13.3.2 defines preconditions evaluation order
+ int next = 1;
+ while (true) {
+ switch (next) {
+ case 1:
+ if (request.getHeader("If-Match") != null) {
+ if (checkIfMatch(request, response, resource)) {
+ next = 3;
+ } else {
+ return false;
+ }
+ } else {
+ next = 2;
+ }
+ break;
+ case 2:
+ if (request.getHeader("If-Unmodified-Since") != null) {
+ if (checkIfUnmodifiedSince(request, response,
resource)) {
+ next = 3;
+ } else {
+ return false;
+ }
+ } else {
+ next = 3;
+ }
+ break;
+ case 3:
+ if (ifNoneMatchHeader != null) {
+ if (checkIfNoneMatch(request, response, resource)) {
+ next = 5;
+ } else {
+ return false;
+ }
+ } else {
+ next = 4;
+ }
+ break;
+ case 4:
+ if (("GET".equals(request.getMethod()) ||
"HEAD".equals(request.getMethod())) &&
+ ifNoneMatchHeader == null &&
request.getHeader("If-Modified-Since") != null) {
+ if (checkIfModifiedSince(request, response, resource))
{
+ next = 5;
+ } else {
+ return false;
+ }
+ } else {
+ next = 5;
+ }
+ break;
+ case 5:
+ if ("GET".equals(request.getMethod()) &&
request.getHeader("If-Range") != null
+ && request.getHeader("Range") != null) {
+ if (checkIfRange(request, response, resource) &&
determineRangeRequestsApplicable(resource)) {
+ // Partial content, precondition passed
+ return true;
+ } else {
+ // ignore the Range header field
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ }
}
@@ -848,15 +910,6 @@ public class DefaultServlet extends HttpServlet {
}
boolean included = false;
- // Check if the conditions specified in the optional If headers are
- // satisfied.
- if (resource.isFile()) {
- // Checking If headers
- included =
(request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
- if (!included && !isError && !checkIfHeaders(request, response,
resource)) {
- return;
- }
- }
// Find content type.
String contentType = resource.getMimeType();
@@ -870,11 +923,21 @@ public class DefaultServlet extends HttpServlet {
// be needed later
String eTag = null;
String lastModifiedHttp = null;
+
if (resource.isFile() && !isError) {
eTag = generateETag(resource);
lastModifiedHttp = resource.getLastModifiedHttp();
}
+ // Check if the conditions specified in the optional If headers are
+ // satisfied.
+ if (resource.isFile()) {
+ // Checking If headers
+ included =
(request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
+ if (!included && !isError && !checkIfHeaders(request, response,
resource)) {
+ return;
+ }
+ }
// Serve a precompressed version of the file if present
boolean usingPrecompressedVersion = false;
@@ -1470,41 +1533,24 @@ public class DefaultServlet extends HttpServlet {
protected Ranges parseRange(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
throws IOException {
- if (!"GET".equals(request.getMethod())) {
+ // Retrieving the range header (if any is specified)
+ String rangeHeader = request.getHeader("Range");
+
+ if (rangeHeader == null) {
+ // No Range header is the same as ignoring any Range header
+ return FULL;
+ }
+
+ if (!"GET".equals(request.getMethod()) ||
!determineRangeRequestsApplicable(resource)) {
// RFC 9110 - Section 14.2: GET is the only method for which range
handling is defined.
// Otherwise MUST ignore a Range header field
return FULL;
}
- // Checking If-Range
- String headerValue = request.getHeader("If-Range");
-
- if (headerValue != null) {
-
- long headerValueTime = (-1L);
- try {
- headerValueTime = request.getDateHeader("If-Range");
- } catch (IllegalArgumentException e) {
- // Ignore
- }
-
- String eTag = generateETag(resource);
- long lastModified = resource.getLastModified();
-
- if (headerValueTime == (-1L)) {
- // If the ETag the client gave does not match the entity
- // etag, then the entire entity is returned.
- if (!eTag.equals(headerValue.trim())) {
- return FULL;
- }
- } else {
- // If the timestamp of the entity the client got differs from
- // the last modification date of the entity, the entire entity
- // is returned.
- if (Math.abs(lastModified - headerValueTime) > 1000) {
- return FULL;
- }
- }
+ // Although If-Range evaluation was performed previously, the result
were not propagated.
+ // Hence we have to evaluate If-Range again.
+ if (!checkIfRange(request, response, resource)) {
+ return FULL;
}
long fileLength = resource.getContentLength();
@@ -1515,13 +1561,6 @@ public class DefaultServlet extends HttpServlet {
return FULL;
}
- // Retrieving the range header (if any is specified)
- String rangeHeader = request.getHeader("Range");
-
- if (rangeHeader == null) {
- // No Range header is the same as ignoring any Range header
- return FULL;
- }
Ranges ranges = Ranges.parse(new StringReader(rangeHeader));
@@ -2166,36 +2205,49 @@ public class DefaultServlet extends HttpServlet {
protected boolean checkIfMatch(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
throws IOException {
- String headerValue = request.getHeader("If-Match");
- if (headerValue != null) {
-
- boolean conditionSatisfied;
+ String resourceETag = generateETag(resource);
+ if (resourceETag == null) {
+ // if a current representation for the target resource is not
present
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return false;
+ }
- if (!headerValue.equals("*")) {
- String resourceETag = generateETag(resource);
- if (resourceETag == null) {
- conditionSatisfied = false;
- } else {
- // RFC 7232 requires strong comparison for If-Match headers
- Boolean matched = EntityTag.compareEntityTag(new
StringReader(headerValue), false, resourceETag);
- if (matched == null) {
- if (debug > 10) {
- log("DefaultServlet.checkIfMatch: Invalid header
value [" + headerValue + "]");
- }
- response.sendError(HttpServletResponse.SC_BAD_REQUEST);
- return false;
+ boolean conditionSatisfied = false;
+ Enumeration<String> headerValues = request.getHeaders("If-Match");
+ if (!headerValues.hasMoreElements()) {
+ return true;
+ }
+ boolean hasAsteriskValue = false;// check existence of special header
value '*'
+ while (headerValues.hasMoreElements() && !conditionSatisfied) {
+ String headerValue = headerValues.nextElement();
+ if ("*".equals(headerValue)) {
+ hasAsteriskValue = true;
+ conditionSatisfied = true;
+ } else {
+ // RFC 7232 requires strong comparison for If-Match headers
+ Boolean matched = EntityTag.compareEntityTag(new
StringReader(headerValue), false, resourceETag);
+ if (matched == null) {
+ if (debug > 10) {
+ log("DefaultServlet.checkIfMatch: Invalid header
value [" + headerValue + "]");
}
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return false;
+ } else {
conditionSatisfied = matched.booleanValue();
}
- } else {
- conditionSatisfied = true;
- }
-
- if (!conditionSatisfied) {
- response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
- return false;
}
}
+ if (hasAsteriskValue && headerValues.hasMoreElements()) {
+ // Note that an If-Match header field with a list value containing
"*" and other values (including other
+ // instances of "*") is syntactically invalid (therefore not
allowed to be generated) and furthermore is
+ // unlikely to be interoperable.
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return false;
+ }
+ if (!conditionSatisfied) {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return false;
+ }
return true;
}
@@ -2212,14 +2264,16 @@ public class DefaultServlet extends HttpServlet {
*/
protected boolean checkIfModifiedSince(HttpServletRequest request,
HttpServletResponse response,
WebResource resource) {
+
+ long resourceLastModified = resource.getLastModified();
+
try {
long headerValue = request.getDateHeader("If-Modified-Since");
- long lastModified = resource.getLastModified();
if (headerValue != -1) {
// If an If-None-Match header has been specified, if modified
since
// is ignored.
- if ((request.getHeader("If-None-Match") == null) &&
(lastModified < headerValue + 1000)) {
+ if ((request.getHeader("If-None-Match") == null) &&
(resourceLastModified < headerValue + 1000)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
@@ -2250,15 +2304,39 @@ public class DefaultServlet extends HttpServlet {
protected boolean checkIfNoneMatch(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
throws IOException {
- String headerValue = request.getHeader("If-None-Match");
- if (headerValue != null) {
+ String resourceETag = generateETag(resource);
- boolean conditionSatisfied;
+ Enumeration<String> headerValues = request.getHeaders("If-None-Match");
+ if (!headerValues.hasMoreElements()) {
+ return true;
+ }
+ boolean hasAsteriskValue = false;// check existence of special header
value '*'
+ boolean conditionSatisfied = true;
+ while (headerValues.hasMoreElements()) {
- String resourceETag = generateETag(resource);
- if (!headerValue.equals("*")) {
- if (resourceETag == null) {
+ String headerValue = headerValues.nextElement();
+
+ if (headerValue.equals("*")) {
+ hasAsteriskValue = true;
+ if (headerValues.hasMoreElements()) {
conditionSatisfied = false;
+ break;
+ } else {
+ // asterisk '*' is the only field value.
+ // RFC9110: If the field value is "*", the condition is
false if the origin server has a current
+ // representation for the target resource.
+ if (resourceETag != null) {
+ conditionSatisfied = false;
+ } else {
+ conditionSatisfied = true;
+ }
+ break;
+ }
+ } else {
+ if (resourceETag == null) {
+ // None of the entity tag matches.
+ conditionSatisfied = true;
+ break;
} else {
// RFC 7232 requires weak comparison for If-None-Match
headers
Boolean matched = EntityTag.compareEntityTag(new
StringReader(headerValue), true, resourceETag);
@@ -2269,25 +2347,37 @@ public class DefaultServlet extends HttpServlet {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
- conditionSatisfied = matched.booleanValue();
+ if (matched.booleanValue()) {
+ // RFC9110: If the field value is a list of entity
tags, the condition is false if one of the
+ // listed tags
+ // matches the entity tag of the selected
representation.
+ conditionSatisfied = false;
+ break;
+ }
}
- } else {
- conditionSatisfied = true;
}
- if (conditionSatisfied) {
- // For GET and HEAD, we should respond with
- // 304 Not Modified.
- // For every other method, 412 Precondition Failed is sent
- // back.
- if ("GET".equals(request.getMethod()) ||
"HEAD".equals(request.getMethod())) {
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
- response.setHeader("ETag", resourceETag);
- } else {
-
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
- }
- return false;
+ }
+
+ if (hasAsteriskValue && headerValues.hasMoreElements()) {
+ // Note that an If-None-Match header field with a list value
containing "*" and other values (including
+ // other instances of "*") is syntactically invalid (therefore not
allowed to be generated) and furthermore
+ // is unlikely to be interoperable.
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return false;
+ }
+ if (!conditionSatisfied) {
+ // For GET and HEAD, we should respond with
+ // 304 Not Modified.
+ // For every other method, 412 Precondition Failed is sent
+ // back.
+ if ("GET".equals(request.getMethod()) ||
"HEAD".equals(request.getMethod())) {
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ response.setHeader("ETag", resourceETag);
+ } else {
+ response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
}
+ return false;
}
return true;
}
@@ -2307,11 +2397,28 @@ public class DefaultServlet extends HttpServlet {
*/
protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
HttpServletResponse response,
WebResource resource) throws IOException {
+
+ long resourceLastModified = resource.getLastModified();
+ if (resourceLastModified <= -1 || request.getHeader("If-Match") !=
null) {
+ // MUST ignore if the resource does not have a modification date
available.
+ // MUST ignore if the request contains an If-Match header field
+ return true;
+ }
+ Enumeration<String> headerEnum =
request.getHeaders("If-Unmodified-Since");
+ if (!headerEnum.hasMoreElements()) {
+ // If-Unmodified-Since is not present
+ return true;
+ }
+ headerEnum.nextElement();
+ if (headerEnum.hasMoreElements()) {
+ // If-Unmodified-Since is a list of dates
+ return true;
+ }
+
try {
- long lastModified = resource.getLastModified();
long headerValue = request.getDateHeader("If-Unmodified-Since");
if (headerValue != -1) {
- if (lastModified >= (headerValue + 1000)) {
+ if (resourceLastModified >= (headerValue + 1000)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
@@ -2325,6 +2432,79 @@ public class DefaultServlet extends HttpServlet {
}
+ /**
+ * Check if the if-range condition is satisfied.
+ *
+ * @param request The servlet request we are processing
+ * @param response The servlet response we are creating
+ * @param resource The resource
+ *
+ * @return <code>true</code> if the resource meets the specified
condition, and <code>false</code> if the condition
+ * is not satisfied, resulting in transfer of the new selected
representation instead of a 412
+ * (Precondition Failed) response.
+ *
+ * @throws IOException an IO error occurred
+ */
+ protected boolean checkIfRange(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
+ throws IOException {
+ String resourceETag = generateETag(resource);
+ long resourceLastModified = resource.getLastModified();
+
+ String headerValue = request.getHeader("If-Range");
+ if (headerValue == null) {
+ return true;
+ }
+
+ String rangeHeader = request.getHeader("Range");
+ if (rangeHeader == null ||
!determineRangeRequestsApplicable(resource)) {
+ // Simply ignore If-Range header field
+ return true;
+ }
+
+ long headerValueTime = (-1L);
+ try {
+ headerValueTime = request.getDateHeader("If-Range");
+ } catch (IllegalArgumentException e) {
+ // Ignore
+ }
+
+ if (headerValueTime == (-1L)) {
+ // If the ETag the client gave does not match the entity
+ // etag, then the entire entity is returned.
+ if (resourceETag != null && resourceETag.startsWith("\"") &&
resourceETag.equals(headerValue.trim())) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ // unit of HTTP date is second, ignore millisecond part.
+ return resourceLastModified >= headerValueTime &&
resourceLastModified < headerValueTime + 1000;
+ }
+ }
+
+ /**
+ * Checks if range request is supported by server
+ *
+ * @return <code>true</code> server supports range requests feature.
+ */
+ protected boolean isRangeRequestsSupported() {
+ // Range-Requests optional feature is enabled implicitly.
+ return true;
+ }
+
+ /**
+ * Determines if range-request is applicable for the target resource.
+ * <p>
+ * Subclass have an opportunity to customize by overriding this method.
+ *
+ * @param resource the target resource
+ *
+ * @return <code>true</code> only if range requests is supported by both
the server and the target resource.
+ */
+ protected boolean determineRangeRequestsApplicable(WebResource resource) {
+ return isRangeRequestsSupported() && resource.isFile() &&
resource.exists();
+ }
+
/**
* Provides the entity tag (the ETag header) for the given resource.
Intended to be over-ridden by custom
* DefaultServlet implementations that wish to use an alternative format
for the entity tag.
diff --git
a/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13.java
b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13.java
new file mode 100644
index 0000000000..f191e6f027
--- /dev/null
+++ b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13.java
@@ -0,0 +1,672 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.catalina.servlets;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.function.IntPredicate;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Wrapper;
+import org.apache.catalina.startup.SimpleHttpClient;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.http.FastHttpDateFormat;
+
+public class TestDefaultServletRfc9110Section13 extends TomcatBaseTest {
+
+ @Test
+ public void testPreconditions2_2_1_head0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
null, null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_IN, null, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null,
null, null, null, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, 400);
+ }
+
+ @Test
+ public void testPreconditions2_2_1_head1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
null, null, null, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_IN, null, null,
null, null, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null,
null, null, null, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, 400);
+ }
+
+ @Test
+ public void testPreconditions2_2_2_head0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_LT, null,
null, null, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN,
null, null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_2_head1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_LT, null,
null, null, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, null,
null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN,
null, null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_3_head0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_IN, null,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_EXACTLY, null, null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_ALL, null, null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_EXACTLY, null, null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_ALL, null, null, 304);
+ }
+
+ @Test
+ public void testPreconditions2_2_3_head1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_EXACTLY, null, null, 304,
+ 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_EXACTLY, null, null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_ALL, null, null, 304);
+ }
+ // @Test
+ // public void testPreconditions2_2_4_head0() throws Exception {
+ // startServer(true);
+ // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_EQ, null, 200);
+ // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_LT, null, 412);
+ // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_GT, null, 200);
+ // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_MULTI, null, 200);
+ // }
+
+ @Test
+ public void testPreconditions2_2_4_head1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_EQ, null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_LT, null, 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_GT, null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, null, null,
IfPolicy.DATE_MULTI_IN, null, 200);
+
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null,
IfPolicy.ETAG_NOT_IN, IfPolicy.DATE_EQ, null,
+ 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_EXACTLY, IfPolicy.DATE_GT,
+ null, 304, 412);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ,
IfPolicy.ETAG_NOT_IN, IfPolicy.DATE_LT, null,
+ 200);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_EXACTLY, IfPolicy.DATE_MULTI_IN,
+ null, 304);
+ testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_ALL, IfPolicy.DATE_EQ, null, 304);
+ }
+
+ @Test
+ public void testPreconditions2_2_1_get0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_1_get1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, 200);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
null, null, null, 412);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_IN, null, null,
null, null, 412);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null,
null, null, null, 412);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, 400);
+ }
+
+ @Test
+ public void testPreconditions2_2_2_get0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, null, 200);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_LT, null,
null, null, 412);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_GT, null,
null, null, 200);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN,
null, null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_2_get1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, null, 200);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_LT, null,
null, null, 412);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_GT, null,
null, null, 200);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN,
null, null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_5_get0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.GET_INDEX_HTML, null, null, null, null,
IfPolicy.DATE_EQ, true, 206);
+ // if-range: multiple node policy, not defined in RFC 9110.
+ // Currently, tomcat process the first If-Range header simply.
+ // testPreconditions(Task.GET_INDEX_HTML, null, null, null, null,
IfPolicy.DATE_MULTI_IN, true,200);
+ testPreconditions(Task.GET_INDEX_HTML, null, null, null, null,
IfPolicy.DATE_SEMANTIC_INVALID, true, 200);
+ testPreconditions(Task.GET_INDEX_HTML, null, null, null, null,
IfPolicy.ETAG_EXACTLY, true, 206);
+
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_EQ, true, 206);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_LT, true, 200);
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_GT, true, 200);
+
+ testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_EQ, false, 200);
+
+ // Test Range header is present, while if-range is not.
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, true, 206);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
null, null, null, true, 206);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_IN, null, null,
null, null, true, 206);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null,
null, null, null, true, 412);
+ testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, true, 400);
+ }
+
+
+ @Test
+ public void testPreconditions2_2_1_post0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_1_post1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
null, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, 400);
+ }
+
+ @Test
+ public void testPreconditions2_2_2_post0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_LT, null,
null, null, false, null,
+ k -> ((k >= 200 && k < 300) || k == 412), -1);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN,
null, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null,
IfPolicy.DATE_SEMANTIC_INVALID, null, null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_2_post1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_LT, null,
null, null, false, null,
+ k -> (k >= 200 && k < 300) || k == 412, -1);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN,
null, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null,
IfPolicy.DATE_SEMANTIC_INVALID, null, null, null, 200);
+ }
+
+ @Test
+ public void testPreconditions2_2_3_post0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.POST_INDEX_HTML, null, null,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null,
IfPolicy.ETAG_EXACTLY, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, IfPolicy.ETAG_ALL,
null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_IN, null,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_EXACTLY, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_ALL, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_EXACTLY, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_ALL, null, null, 412);
+ }
+
+ @Test
+ public void testPreconditions2_2_3_post1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.POST_INDEX_HTML, null, null,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null,
IfPolicy.ETAG_EXACTLY, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, IfPolicy.ETAG_ALL,
null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_EXACTLY, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
IfPolicy.ETAG_ALL, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ,
IfPolicy.ETAG_NOT_IN, null, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_EXACTLY, null, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT,
IfPolicy.ETAG_ALL, null, null, 412);
+ }
+
+ @Test
+ public void testPreconditions2_2_4_post1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null,
IfPolicy.DATE_EQ, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null,
IfPolicy.DATE_LT, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null,
IfPolicy.DATE_GT, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null,
IfPolicy.DATE_MULTI_IN, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null,
IfPolicy.ETAG_NOT_IN, IfPolicy.DATE_EQ, null, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null,
IfPolicy.ETAG_EXACTLY, IfPolicy.DATE_LT, null, 412);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, IfPolicy.ETAG_ALL,
IfPolicy.DATE_MULTI_IN, null, 412);
+ }
+
+ @Test
+ public void testPreconditions2_2_5_post0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null, null,
IfPolicy.DATE_EQ, true, 200);
+ // if-range: multiple node policy, not defined in RFC 9110.
+ // Currently, tomcat process the first If-Range header simply.
+ // testPreconditions(Task.GET_INDEX_HTML, null, null, null, null,
IfPolicy.DATE_MULTI_IN, true,200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null, null,
IfPolicy.DATE_SEMANTIC_INVALID, true, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, null, null, null,
IfPolicy.ETAG_EXACTLY, true, 200);
+
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_EQ, true, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_LT, true, 200);
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_GT, true, 200);
+
+ testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null,
null, IfPolicy.DATE_EQ, false, 200);
+
+ // Test Range header is present, while if-range is not.
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, null,
null, null, true, 200);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null,
null, null, null, true, 200);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_IN, null, null,
null, null, true, 200);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null,
null, null, null, true, 412);
+ testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, true, 400);
+ }
+
+ @Ignore
+ @Test
+ public void testPreconditions2_2_1_put0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_ALL, null, null,
null, null,
+ HttpServletResponse.SC_NO_CONTENT);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_IN, null, null,
null, null,
+ HttpServletResponse.SC_NO_CONTENT);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_NOT_IN, null,
null, null, null, 412);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, 400);
+
+ testPreconditions(Task.PUT_NEW_TXT, null, null, null, null, null,
HttpServletResponse.SC_CREATED);
+ }
+
+ @Ignore
+ @Test
+ public void testPreconditions2_2_1_put1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_ALL, null, null,
null, null,
+ HttpServletResponse.SC_NO_CONTENT);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_SYNTAX_INVALID,
null, null, null, null, 400);
+ testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_EXACTLY, null,
null, null, null, 412);
+ }
+
+ @Ignore
+ @Test
+ public void testPreconditions2_2_1_delete0() throws Exception {
+ startServer(true);
+ testPreconditions(Task.DELETE_EXIST1_TXT, IfPolicy.ETAG_ALL, null,
null, null, null,
+ HttpServletResponse.SC_NO_CONTENT);
+ testPreconditions(Task.DELETE_EXIST2_TXT, IfPolicy.ETAG_IN, null,
null, null, null,
+ HttpServletResponse.SC_NO_CONTENT);
+ testPreconditions(Task.DELETE_EXIST3_TXT, IfPolicy.ETAG_NOT_IN, null,
null, null, null, 412);
+ testPreconditions(Task.DELETE_EXIST4_TXT,
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null, null, 400);
+
+ testPreconditions(Task.DELETE_NOT_EXIST_TXT, null, null, null, null,
null, 404);
+ }
+
+ @Ignore
+ @Test
+ public void testPreconditions2_2_1_delete1() throws Exception {
+ startServer(false);
+ testPreconditions(Task.DELETE_EXIST1_TXT, IfPolicy.ETAG_ALL, null,
null, null, null,
+ HttpServletResponse.SC_NO_CONTENT);
+ testPreconditions(Task.DELETE_EXIST3_TXT, IfPolicy.ETAG_EXACTLY, null,
null, null, null, 412);
+ testPreconditions(Task.DELETE_EXIST2_TXT,
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null, null, 400);
+ }
+
+ enum HTTP_METHOD {
+ GET,
+ PUT,
+ DELETE,
+ POST,
+ HEAD
+ }
+
+ enum Task {
+ HEAD_INDEX_HTML(HTTP_METHOD.HEAD, "/index.html"),
+ HEAD_404_HTML(HTTP_METHOD.HEAD, "/sc_404.html"),
+
+ GET_INDEX_HTML(HTTP_METHOD.GET, "/index.html"),
+ GET_404_HTML(HTTP_METHOD.GET, "/sc_404.html"),
+
+ POST_INDEX_HTML(HTTP_METHOD.POST, "/index.html"),
+ POST_404_HTML(HTTP_METHOD.POST, "/sc_404.html"),
+
+ PUT_EXIST_TXT(HTTP_METHOD.PUT, "/put_exist.txt"),
+ PUT_NEW_TXT(HTTP_METHOD.PUT, "/put_new.txt"),
+
+ DELETE_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_exist.txt"),
+ DELETE_EXIST1_TXT(HTTP_METHOD.DELETE, "/delete_exist1.txt"),
+ DELETE_EXIST2_TXT(HTTP_METHOD.DELETE, "/delete_exist2.txt"),
+ DELETE_EXIST3_TXT(HTTP_METHOD.DELETE, "/delete_exist3.txt"),
+ DELETE_EXIST4_TXT(HTTP_METHOD.DELETE, "/delete_exist4.txt"),
+ DELETE_NOT_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_404.txt");
+
+ HTTP_METHOD m;
+ String uri;
+
+ Task(HTTP_METHOD m, String uri) {
+ this.m = m;
+ this.uri = uri;
+ }
+
+ @Override
+ public String toString() {
+ return m.name() + " " + uri;
+ }
+ }
+
+ enum IfPolicy {
+ ETAG_EXACTLY,
+ ETAG_IN,
+ ETAG_ALL,
+ ETAG_NOT_IN,
+ ETAG_SYNTAX_INVALID,
+ /**
+ * Condition header value of http date is equivalent to actual
resource lastModified date
+ */
+ DATE_EQ,
+ /**
+ * Condition header value of http date is greater(later) than actual
resource lastModified date
+ */
+ DATE_GT,
+ /**
+ * Condition header value of http date is less(earlier) than actual
resource lastModified date
+ */
+ DATE_LT,
+ DATE_MULTI_IN,
+ /**
+ * not a valid HTTP-date
+ */
+ DATE_SEMANTIC_INVALID;
+ }
+
+ enum IfType {
+ ifMatch("If-Match"), // ETag strong comparison
+ ifUnmodifiedSince("If-Unmodified-Since"),
+ ifNoneMatch("If-None-Match"), // ETag weak comparison
+ ifModifiedSince("If-Modified-Since"),
+ ifRange("If-Range"); // ETag strong comparison
+
+ private String header;
+
+ IfType(String header) {
+ this.header = header;
+ }
+
+ public String value() {
+ return this.header;
+ }
+ }
+
+ protected List<String> genETagCondtion(String strongETag, String weakETag,
IfPolicy policy) {
+ List<String> headerValues = new ArrayList<String>();
+ switch (policy) {
+ case ETAG_ALL:
+ headerValues.add("*");
+ break;
+ case ETAG_EXACTLY:
+ if (strongETag != null) {
+ headerValues.add(strongETag);
+ } else {
+ // Should not happen
+ throw new IllegalArgumentException("strong etag not
found!");
+ }
+ break;
+ case ETAG_IN:
+ headerValues.add("\"1a2b3c4d\"");
+ headerValues.add(weakETag + "," + strongETag + ",W/\"*\"");
+ headerValues.add("\"abcdefg\"");
+ break;
+ case ETAG_NOT_IN:
+ if (weakETag != null && weakETag.length() > 8) {
+ headerValues.add(weakETag.substring(0, 3) +
"XXXXX"+weakETag.substring(8));
+ }
+ if (strongETag != null && strongETag.length() > 6) {
+ headerValues.add(strongETag.substring(0, 1) +
"XXXXX"+strongETag.substring(6));
+ }
+ break;
+ case ETAG_SYNTAX_INVALID:
+ headerValues.add("*");
+ headerValues.add("W/\"1abcd\"");
+ break;
+ default:
+ break;
+ }
+ return headerValues;
+ }
+
+ protected List<String> genDateCondtion(long lastModifiedTimestamp,
IfPolicy policy) {
+ List<String> headerValues = new ArrayList<String>();
+ if (lastModifiedTimestamp <= 0) {
+ return headerValues;
+ }
+ switch (policy) {
+ case DATE_EQ:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+ break;
+ case DATE_GT:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+ break;
+ case DATE_LT:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+ break;
+ case DATE_MULTI_IN:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+ break;
+ case DATE_SEMANTIC_INVALID:
+ headerValues.add("2024.12.09 GMT");
+ break;
+ default:
+ break;
+ }
+ return headerValues;
+ }
+
+ protected void wrapperHeaders(Map<String,List<String>> headers, String
resourceETag, long lastModified,
+ IfPolicy policy, IfType type) {
+ Objects.requireNonNull(type);
+ if (policy == null) {
+ return;
+ }
+ List<String> headerValues = new ArrayList<String>();
+ String weakETag = resourceETag;
+ String strongETag = resourceETag;
+ if (resourceETag != null) {
+ if (resourceETag.startsWith("W/")) {
+ strongETag = resourceETag.substring(2);
+ } else {
+ weakETag = "W/" + resourceETag;
+ }
+ }
+
+ List<String> eTagConditions = genETagCondtion(strongETag, weakETag,
policy);
+ if (!eTagConditions.isEmpty()) {
+ headerValues.addAll(eTagConditions);
+ }
+
+ List<String> dateConditions = genDateCondtion(lastModified, policy);
+ if (!dateConditions.isEmpty()) {
+ headerValues.addAll(dateConditions);
+ }
+
+ if (!headerValues.isEmpty()) {
+ headers.put(type.value(), headerValues);
+ }
+ }
+
+ private File tempDocBase = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ tempDocBase =
Files.createTempDirectory(getTemporaryDirectory().toPath(),
"conditional").toFile();
+ long lastModified = FastHttpDateFormat.parseDate("Fri, 06 Dec 2024
00:00:00 GMT");
+ Files.write(Path.of(tempDocBase.getAbsolutePath(), "index.html"),
"<html><body>Index</body></html>".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"index.html").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(), "put_exist.txt"),
"put_exist_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"put_exist.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist.txt"), "delete_exist_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist1.txt"), "delete_exist1_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist1.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist2.txt"), "delete_exist2_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist2.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist3.txt"), "delete_exist3_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist3.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist4.txt"), "delete_exist4_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist4.txt").toFile().setLastModified(lastModified);
+
+ }
+
+ protected void startServer(boolean resourceHasStrongETag) throws Exception
{
+ Tomcat tomcat = getTomcatInstance();
+ Context ctxt = tomcat.addContext("", tempDocBase.getAbsolutePath());
+
+ Wrapper w = Tomcat.addServlet(ctxt, "default",
DefaultServlet.class.getName());
+ w.addInitParameter("readonly", "false");
+ w.addInitParameter("allowPartialPut", Boolean.toString(true));
+ w.addInitParameter("useStrongETags",
Boolean.toString(resourceHasStrongETag));
+ ctxt.addServletMappingDecoded("/", "default");
+
+ tomcat.start();
+ }
+
+
+ protected void testPreconditions(Task task, IfPolicy ifMatchHeader,
IfPolicy ifUnmodifiedSinceHeader,
+ IfPolicy ifNoneMatchHeader, IfPolicy ifModifiedSinceHeader,
IfPolicy ifRangeHeader, boolean autoRangeHeader,
+ String message, IntPredicate p, int... scExpected) throws
Exception {
+ Assert.assertNotNull(task);
+
+
+ Map<String,List<String>> requestHeaders = new HashMap<>();
+
+ Map<String,List<String>> responseHeaders = new HashMap<>();
+
+ String etag = null;
+ long lastModified = -1;
+ String uri = "http://localhost:" + getPort() + task.uri;
+ // Try head to receives etag and lastModified Date
+ int sc = headUrl(uri, new ByteChunk(), responseHeaders);
+ if (sc == 200) {
+ etag = getSingleHeader("ETag", responseHeaders);
+ String dt = getSingleHeader("Last-Modified", responseHeaders);
+ if (dt != null && dt.length() > 0) {
+ lastModified = FastHttpDateFormat.parseDate(dt);
+ }
+ }
+
+ wrapperHeaders(requestHeaders, etag, lastModified, ifMatchHeader,
IfType.ifMatch);
+ wrapperHeaders(requestHeaders, etag, lastModified,
ifModifiedSinceHeader, IfType.ifModifiedSince);
+ wrapperHeaders(requestHeaders, etag, lastModified, ifNoneMatchHeader,
IfType.ifNoneMatch);
+ wrapperHeaders(requestHeaders, etag, lastModified,
ifUnmodifiedSinceHeader, IfType.ifUnmodifiedSince);
+ wrapperHeaders(requestHeaders, etag, lastModified, ifRangeHeader,
IfType.ifRange);
+ responseHeaders.clear();
+ sc = 0;
+ SimpleHttpClient client = null;
+ client = new SimpleHttpClient() {
+
+ @Override
+ public boolean isResponseBodyOK() {
+ return true;
+ }
+ };
+ client.setPort(getPort());
+ StringBuffer curl = new StringBuffer();
+ curl.append(task.m.name() + " " + task.uri + " HTTP/1.1" +
SimpleHttpClient.CRLF + "Host: localhost" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF);
+
+ for (Entry<String,List<String>> e : requestHeaders.entrySet()) {
+ for (String v : e.getValue()) {
+ curl.append(e.getKey() + ": " + v + SimpleHttpClient.CRLF);
+ }
+ }
+ if (autoRangeHeader) {
+ curl.append("Range: bytes=0-10" + SimpleHttpClient.CRLF);
+ }
+ curl.append("Content-Length: 6" + SimpleHttpClient.CRLF);
+ curl.append(SimpleHttpClient.CRLF);
+
+ curl.append("PUT_v2");
+ client.setRequest(new String[] { curl.toString() });
+ client.connect();
+ client.processRequest();
+ for (String e : client.getResponseHeaders()) {
+ Assert.assertTrue("Separator ':' expected and not the last char of
response header field `" + e + "`",
+ e.contains(":") && e.indexOf(':') < e.length() - 1);
+ String name = e.substring(0, e.indexOf(':'));
+ String value = e.substring(e.indexOf(':') + 1);
+ responseHeaders.computeIfAbsent(name, k -> new
ArrayList<String>()).add(value);
+ }
+ sc = client.getStatusCode();
+ if (message == null) {
+ message = "Unexpected status code:`" + sc + "`";
+ }
+ boolean test = false;
+ boolean usePredicate = false;
+ if (scExpected != null && scExpected.length > 0 && scExpected[0] >=
100) {
+ test = Arrays.binarySearch(scExpected, sc) >= 0;
+ } else {
+ usePredicate = true;
+ test = p.test(sc);
+ }
+ String scExpectation = usePredicate ? "IntPredicate" :
Arrays.toString(scExpected);
+ Assert.assertTrue(
+ "Failure - sc expected:%s, sc actual:%d, %s, task:%s, \ntarget
resource:(%s,%s), \nreq headers: %s, \nresp headers: %s"
+ .formatted(scExpectation, sc, message, task, etag,
FastHttpDateFormat.formatDate(lastModified),
+ requestHeaders.toString(),
responseHeaders.toString()),
+ test);
+ }
+
+ protected void testPreconditions(Task task, IfPolicy ifMatchHeader,
IfPolicy ifUnmodifiedSinceHeader,
+ IfPolicy ifNoneMatchHeader, IfPolicy ifModifiedSinceHeader,
IfPolicy ifRangeHeader, int... scExpected)
+ throws Exception {
+ testPreconditions(task, ifMatchHeader, ifUnmodifiedSinceHeader,
ifNoneMatchHeader, ifModifiedSinceHeader,
+ ifRangeHeader, false, scExpected);
+ }
+
+ protected void testPreconditions(Task task, IfPolicy ifMatchHeader,
IfPolicy ifUnmodifiedSinceHeader,
+ IfPolicy ifNoneMatchHeader, IfPolicy ifModifiedSinceHeader,
IfPolicy ifRangeHeader, boolean autoRangeHeader,
+ int... scExpected) throws Exception {
+ testPreconditions(task, ifMatchHeader, ifUnmodifiedSinceHeader,
ifNoneMatchHeader, ifModifiedSinceHeader,
+ ifRangeHeader, autoRangeHeader, null, null, scExpected);
+ }
+}
diff --git
a/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13Parameterized.java
b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13Parameterized.java
new file mode 100644
index 0000000000..192b9ffc75
--- /dev/null
+++
b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13Parameterized.java
@@ -0,0 +1,433 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.catalina.servlets;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.function.IntPredicate;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Wrapper;
+import org.apache.catalina.startup.SimpleHttpClient;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.http.FastHttpDateFormat;
+
+/**
+ * This test case is used to verify RFC 9110 Section 13. Conditional Requests.
+ */
+@RunWith(Parameterized.class)
+public class TestDefaultServletRfc9110Section13Parameterized extends
TomcatBaseTest {
+ @Parameter(0)
+ public boolean useStrongETags;
+ @Parameter(1)
+ public Task task;
+ @Parameter(2)
+ public IfPolicy ifMatchHeader;
+ @Parameter(3)
+ public IfPolicy ifUnmodifiedSinceHeader;
+ @Parameter(4)
+ public IfPolicy ifNoneMatchHeader;
+ @Parameter(5)
+ public IfPolicy ifModifiedSinceHeader;
+ @Parameter(6)
+ public IfPolicy ifRangeHeader;
+ @Parameter(7)
+ public boolean autoRangeHeader;
+ @Parameter(8)
+ public IntPredicate p;
+ @Parameter(9)
+ public int[] scExpected;
+
+ @Parameterized.Parameters(name = "{index} resource-strong [{0}],
matchHeader [{1}]")
+ public static Collection<Object[]> parameters() {
+ List<Object[]> parameterSets = new ArrayList<>();
+ // testPreconditions_rfc9110_13_2_2_1_head0
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_ALL, null, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_EXACTLY, null, null, null, null,
+ false, null, new int[] { 200 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_IN, null, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_NOT_IN, null, null, null, null,
+ false, null, new int[] { 412 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null,
+ null, false, null, new int[] { 400 } });
+
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_ALL, null, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_EXACTLY, null, null, null, null,
+ false, null, new int[] { 412 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_IN, null, null, null, null, false,
+ null, new int[] { 412 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_NOT_IN, null, null, null, null,
+ false, null, new int[] { 412 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML,
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null,
+ null, false, null, new int[] { 400 } });
+
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_EQ, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_LT, null, null, null, false,
+ null, new int[] { 412 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_GT, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_MULTI_IN, null, null, null,
+ false, null, new int[] { 200 } });
+
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_EQ, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_LT, null, null, null, false,
+ null, new int[] { 412 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_GT, null, null, null, false,
+ null, new int[] { 200 } });
+ parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null,
IfPolicy.DATE_MULTI_IN, null, null, null,
+ false, null, new int[] { 200 } });
+
+
+ return parameterSets;
+ }
+
+
+ enum HTTP_METHOD {
+ GET,
+ PUT,
+ DELETE,
+ POST,
+ HEAD
+ }
+
+ enum Task {
+ HEAD_INDEX_HTML(HTTP_METHOD.HEAD, "/index.html"),
+ HEAD_404_HTML(HTTP_METHOD.HEAD, "/sc_404.html"),
+
+ GET_INDEX_HTML(HTTP_METHOD.GET, "/index.html"),
+ GET_404_HTML(HTTP_METHOD.GET, "/sc_404.html"),
+
+ POST_INDEX_HTML(HTTP_METHOD.POST, "/index.html"),
+ POST_404_HTML(HTTP_METHOD.POST, "/sc_404.html"),
+
+ PUT_EXIST_TXT(HTTP_METHOD.PUT, "/put_exist.txt"),
+ PUT_NEW_TXT(HTTP_METHOD.PUT, "/put_new.txt"),
+
+ DELETE_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_exist.txt"),
+ DELETE_EXIST1_TXT(HTTP_METHOD.DELETE, "/delete_exist1.txt"),
+ DELETE_EXIST2_TXT(HTTP_METHOD.DELETE, "/delete_exist2.txt"),
+ DELETE_EXIST3_TXT(HTTP_METHOD.DELETE, "/delete_exist3.txt"),
+ DELETE_EXIST4_TXT(HTTP_METHOD.DELETE, "/delete_exist4.txt"),
+ DELETE_NOT_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_404.txt");
+
+ HTTP_METHOD m;
+ String uri;
+
+ Task(HTTP_METHOD m, String uri) {
+ this.m = m;
+ this.uri = uri;
+ }
+
+ @Override
+ public String toString() {
+ return m.name() + " " + uri;
+ }
+ }
+
+ enum IfPolicy {
+ ETAG_EXACTLY,
+ ETAG_IN,
+ ETAG_ALL,
+ ETAG_NOT_IN,
+ ETAG_SYNTAX_INVALID,
+ /**
+ * Condition header value of http date is equivalent to actual
resource lastModified date
+ */
+ DATE_EQ,
+ /**
+ * Condition header value of http date is greater(later) than actual
resource lastModified date
+ */
+ DATE_GT,
+ /**
+ * Condition header value of http date is less(earlier) than actual
resource lastModified date
+ */
+ DATE_LT,
+ DATE_MULTI_IN,
+ /**
+ * not a valid HTTP-date
+ */
+ DATE_SEMANTIC_INVALID;
+ }
+
+ enum IfType {
+ ifMatch("If-Match"), // ETag strong comparison
+ ifUnmodifiedSince("If-Unmodified-Since"),
+ ifNoneMatch("If-None-Match"), // ETag weak comparison
+ ifModifiedSince("If-Modified-Since"),
+ ifRange("If-Range"); // ETag strong comparison
+
+ private String header;
+
+ IfType(String header) {
+ this.header = header;
+ }
+
+ public String value() {
+ return this.header;
+ }
+ }
+
+ protected List<String> genETagCondtion(String strongETag, String weakETag,
IfPolicy policy) {
+ List<String> headerValues = new ArrayList<String>();
+ switch (policy) {
+ case ETAG_ALL:
+ headerValues.add("*");
+ break;
+ case ETAG_EXACTLY:
+ if (strongETag != null) {
+ headerValues.add(strongETag);
+ } else {
+ // Should not happen
+ throw new IllegalArgumentException("strong etag not
found!");
+ }
+ break;
+ case ETAG_IN:
+ headerValues.add("\"1a2b3c4d\"");
+ headerValues.add(weakETag + "," + strongETag + ",W/\"*\"");
+ headerValues.add("\"abcdefg\"");
+ break;
+ case ETAG_NOT_IN:
+ if (weakETag != null && weakETag.length() > 8) {
+ headerValues.add(weakETag.substring(0, 3) + "XXXXX" +
weakETag.substring(8));
+ }
+ if (strongETag != null && strongETag.length() > 6) {
+ headerValues.add(strongETag.substring(0, 1) + "XXXXX" +
strongETag.substring(6));
+ }
+ break;
+ case ETAG_SYNTAX_INVALID:
+ headerValues.add("*");
+ headerValues.add("W/\"1abcd\"");
+ break;
+ default:
+ break;
+ }
+ return headerValues;
+ }
+
+ protected List<String> genDateCondtion(long lastModifiedTimestamp,
IfPolicy policy) {
+ List<String> headerValues = new ArrayList<String>();
+ if (lastModifiedTimestamp <= 0) {
+ return headerValues;
+ }
+ switch (policy) {
+ case DATE_EQ:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+ break;
+ case DATE_GT:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+ break;
+ case DATE_LT:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+ break;
+ case DATE_MULTI_IN:
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+ break;
+ case DATE_SEMANTIC_INVALID:
+ headerValues.add("2024.12.09 GMT");
+ break;
+ default:
+ break;
+ }
+ return headerValues;
+ }
+
+ protected void wrapperHeaders(Map<String,List<String>> headers, String
resourceETag, long lastModified,
+ IfPolicy policy, IfType type) {
+ Objects.requireNonNull(type);
+ if (policy == null) {
+ return;
+ }
+ List<String> headerValues = new ArrayList<String>();
+ String weakETag = resourceETag;
+ String strongETag = resourceETag;
+ if (resourceETag != null) {
+ if (resourceETag.startsWith("W/")) {
+ strongETag = resourceETag.substring(2);
+ } else {
+ weakETag = "W/" + resourceETag;
+ }
+ }
+
+ List<String> eTagConditions = genETagCondtion(strongETag, weakETag,
policy);
+ if (!eTagConditions.isEmpty()) {
+ headerValues.addAll(eTagConditions);
+ }
+
+ List<String> dateConditions = genDateCondtion(lastModified, policy);
+ if (!dateConditions.isEmpty()) {
+ headerValues.addAll(dateConditions);
+ }
+
+ if (!headerValues.isEmpty()) {
+ headers.put(type.value(), headerValues);
+ }
+ }
+
+ private File tempDocBase = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ tempDocBase =
Files.createTempDirectory(getTemporaryDirectory().toPath(),
"conditional").toFile();
+ long lastModified = FastHttpDateFormat.parseDate("Fri, 06 Dec 2024
00:00:00 GMT");
+ Files.write(Path.of(tempDocBase.getAbsolutePath(), "index.html"),
"<html><body>Index</body></html>".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"index.html").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(), "put_exist.txt"),
"put_exist_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"put_exist.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist.txt"), "delete_exist_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist1.txt"), "delete_exist1_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist1.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist2.txt"), "delete_exist2_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist2.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist3.txt"), "delete_exist3_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist3.txt").toFile().setLastModified(lastModified);
+
+ Files.write(Path.of(tempDocBase.getAbsolutePath(),
"delete_exist4.txt"), "delete_exist4_v0".getBytes(),
+ StandardOpenOption.CREATE);
+ Path.of(tempDocBase.getAbsolutePath(),
"delete_exist4.txt").toFile().setLastModified(lastModified);
+
+ }
+
+ @Test
+ public void testPreconditions() throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+ Context ctxt = tomcat.addContext("", tempDocBase.getAbsolutePath());
+
+ Wrapper w = Tomcat.addServlet(ctxt, "default",
DefaultServlet.class.getName());
+ w.addInitParameter("readonly", "false");
+ w.addInitParameter("allowPartialPut", Boolean.toString(true));
+ w.addInitParameter("useStrongETags", Boolean.toString(useStrongETags));
+ ctxt.addServletMappingDecoded("/", "default");
+
+ tomcat.start();
+
+ Assert.assertNotNull(task);
+
+
+ Map<String,List<String>> requestHeaders = new HashMap<>();
+
+ Map<String,List<String>> responseHeaders = new HashMap<>();
+
+ String etag = null;
+ long lastModified = -1;
+ String uri = "http://localhost:" + getPort() + task.uri;
+ // Try head to receives etag and lastModified Date
+ int sc = headUrl(uri, new ByteChunk(), responseHeaders);
+ if (sc == 200) {
+ etag = getSingleHeader("ETag", responseHeaders);
+ String dt = getSingleHeader("Last-Modified", responseHeaders);
+ if (dt != null && dt.length() > 0) {
+ lastModified = FastHttpDateFormat.parseDate(dt);
+ }
+ }
+
+ wrapperHeaders(requestHeaders, etag, lastModified, ifMatchHeader,
IfType.ifMatch);
+ wrapperHeaders(requestHeaders, etag, lastModified,
ifModifiedSinceHeader, IfType.ifModifiedSince);
+ wrapperHeaders(requestHeaders, etag, lastModified, ifNoneMatchHeader,
IfType.ifNoneMatch);
+ wrapperHeaders(requestHeaders, etag, lastModified,
ifUnmodifiedSinceHeader, IfType.ifUnmodifiedSince);
+ wrapperHeaders(requestHeaders, etag, lastModified, ifRangeHeader,
IfType.ifRange);
+ responseHeaders.clear();
+ sc = 0;
+ SimpleHttpClient client = null;
+ client = new SimpleHttpClient() {
+
+ @Override
+ public boolean isResponseBodyOK() {
+ return true;
+ }
+ };
+ client.setPort(getPort());
+ StringBuffer curl = new StringBuffer();
+ curl.append(task.m.name() + " " + task.uri + " HTTP/1.1" +
SimpleHttpClient.CRLF + "Host: localhost" +
+ SimpleHttpClient.CRLF + "Connection: Close" +
SimpleHttpClient.CRLF);
+
+ for (Entry<String,List<String>> e : requestHeaders.entrySet()) {
+ for (String v : e.getValue()) {
+ curl.append(e.getKey() + ": " + v + SimpleHttpClient.CRLF);
+ }
+ }
+ if (autoRangeHeader) {
+ curl.append("Range: bytes=0-10" + SimpleHttpClient.CRLF);
+ }
+ curl.append("Content-Length: 6" + SimpleHttpClient.CRLF);
+ curl.append(SimpleHttpClient.CRLF);
+
+ curl.append("PUT_v2");
+ client.setRequest(new String[] { curl.toString() });
+ client.connect();
+ client.processRequest();
+ for (String e : client.getResponseHeaders()) {
+ Assert.assertTrue("Separator ':' expected and not the last char of
response header field `" + e + "`",
+ e.contains(":") && e.indexOf(':') < e.length() - 1);
+ String name = e.substring(0, e.indexOf(':'));
+ String value = e.substring(e.indexOf(':') + 1);
+ responseHeaders.computeIfAbsent(name, k -> new
ArrayList<String>()).add(value);
+ }
+ sc = client.getStatusCode();
+ boolean test = false;
+ boolean usePredicate = false;
+ if (scExpected != null && scExpected.length > 0 && scExpected[0] >=
100) {
+ test = Arrays.binarySearch(scExpected, sc) >= 0;
+ } else {
+ usePredicate = true;
+ test = p.test(sc);
+ }
+ String scExpectation = usePredicate ? "IntPredicate" :
Arrays.toString(scExpected);
+ Assert.assertTrue(
+ "Failure - sc expected:%s, sc actual:%d, task:%s, \ntarget
resource:(%s,%s), \nreq headers: %s, \nresp headers: %s"
+ .formatted(scExpectation, sc, task, etag,
FastHttpDateFormat.formatDate(lastModified),
+ requestHeaders.toString(),
responseHeaders.toString()),
+ test);
+ }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index daa677ff46..7edc0d6391 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -112,6 +112,10 @@
<code>DataSourcePropertyStore</code> that may be used by the WebDAV
Servlet. (remm)
</update>
+ <update>
+ Improve HTTP If headers processing according to RFC 9110. Based on pull
+ request <pr>796</pr> by Chenjp. (remm)
+ </update>
</changelog>
</subsection>
</section>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]