/*
* 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 jakarta.servlet.http.HttpServletResponse;
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 TestDefaultServletRfc9110Section13 extends TomcatBaseTest {
@Parameter(0)
public boolean useStrongETags;
@Parameter(1)
public Task task;
@Parameter(2)
public EtagPrecondition ifMatchPrecondition;
@Parameter(3)
public DatePrecondition ifUnmodifiedSincePrecondition;
@Parameter(4)
public EtagPrecondition ifNoneMatchPrecondition;
@Parameter(5)
public DatePrecondition ifModifiedSincePrecondition;
@Parameter(6)
public EtagPrecondition ifRangeEtagPrecondition;
@Parameter(7)
public DatePrecondition ifRangeDatePrecondition;
@Parameter(8)
public boolean addRangeHeader;
@Parameter(9)
public Integer scExpected;
@Parameterized.Parameters(name = "{index} resource-strong [{0}], matchHeader [{1}]")
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
for (Boolean useStrongEtag : booleans) {
for (Task task : Arrays.asList(Task.HEAD_INDEX_HTML, Task.GET_INDEX_HTML, Task.POST_INDEX_HTML)) {
// RFC 9110, Section 13.2.2, Step 1, HEAD: If-Match with and without If-Unmodified-Since
for (DatePrecondition dateCondition : DatePrecondition.values()) {
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.ALL, dateCondition, null,
null, null, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.EXACTLY, dateCondition, null,
null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_200 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.IN, dateCondition, null,
null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_200 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.NOT_IN, dateCondition, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID, dateCondition, null,
null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
dateCondition, null, null, null, null, Boolean.FALSE, SC_400 });
}
// RFC 9110, Section 13.2.2, Step 2, HEAD: If-Unmodified-Since only
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.EQ, null, null, null, null,
Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null, null, null, null,
Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.GT, null, null, null, null,
Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN, null, null, null,
null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN_REV, null, null,
null, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.INVALID, null, null, null,
null, Boolean.FALSE, SC_200 });
// Ensure If-Unmodified-Since takes precedence over If-Modified-Since
// If-Unmodified-Since only
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null, null, null, null,
Boolean.FALSE, SC_412 });
// If-Modified-Since only
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.GT, null, null,
Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_200 : SC_304 });
// Both
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null,
DatePrecondition.GT, null, null, Boolean.FALSE, SC_412 });
// RFC 9110, Section 13.2.2, Step 3, HEAD: If-None-Match with and without If-Modified-Since
for (DatePrecondition dateCondition : DatePrecondition.values()) {
parameterSets
.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.ALL, dateCondition,
null, null, Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_412 : SC_304 });
parameterSets.add(
new Object[] { useStrongEtag, task, null, null, EtagPrecondition.EXACTLY, dateCondition,
null, null, Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_412 : SC_304 });
parameterSets
.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.IN, dateCondition,
null, null, Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_412 : SC_304 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.NOT_IN,
dateCondition, null, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID,
dateCondition, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(
new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
dateCondition, null, null, Boolean.FALSE, SC_400 });
}
// RFC 9110, Section 13.2.2, Step 4, HEAD: If-Modified-Since only
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.EQ, null, null,
Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_200 : SC_304 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.LT, null, null,
Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.GT, null, null,
Boolean.FALSE, task.equals(Task.POST_INDEX_HTML) ? SC_200 : SC_304 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN, null,
null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN_REV,
null, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.INVALID, null,
null, Boolean.FALSE, SC_200 });
}
for (Task task : Arrays.asList(Task.HEAD_404_HTML, Task.GET_404_HTML, Task.POST_404_HTML)) {
// RFC 9110, Section 13.2.2, Step 1, HEAD: If-Match with and without If-Unmodified-Since
for (DatePrecondition dateCondition : DatePrecondition.values()) {
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.ALL, dateCondition, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.IN, dateCondition, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.NOT_IN, dateCondition, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID, dateCondition, null,
null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, task, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
dateCondition, null, null, null, null, Boolean.FALSE, SC_400 });
}
// RFC 9110, Section 13.2.2, Step 2, HEAD: If-Unmodified-Since only
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.EQ, null, null, null, null,
Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.LT, null, null, null, null,
Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.GT, null, null, null, null,
Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN, null, null, null,
null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.MULTI_IN_REV, null, null,
null, null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, DatePrecondition.INVALID, null, null, null,
null, Boolean.FALSE, SC_404 });
// RFC 9110, Section 13.2.2, Step 3, HEAD: If-None-Match with and without If-Modified-Since
for (DatePrecondition dateCondition : DatePrecondition.values()) {
parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.ALL,
dateCondition, null, null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.IN,
dateCondition, null, null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.NOT_IN,
dateCondition, null, null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID,
dateCondition, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(
new Object[] { useStrongEtag, task, null, null, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
dateCondition, null, null, Boolean.FALSE, SC_400 });
}
// RFC 9110, Section 13.2.2, Step 4, HEAD: If-Modified-Since only
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.EQ, null, null,
Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.LT, null, null,
Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.GT, null, null,
Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN, null,
null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.MULTI_IN_REV,
null, null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, task, null, null, null, DatePrecondition.INVALID, null,
null, Boolean.FALSE, SC_404 });
}
// RFC 9110, Section 13.2.2, Step 5, GET: If-Range only
// entity-tag
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.ALL, null, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.EXACTLY, null, Boolean.TRUE, useStrongEtag.booleanValue() ? SC_206 : SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.IN, null, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.NOT_IN, null, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.INVALID, null, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, Boolean.TRUE, SC_400 });
// HTTP-date
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.EQ, Boolean.TRUE, SC_206 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.LT, Boolean.TRUE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.GT, Boolean.TRUE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.MULTI_IN, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.MULTI_IN_REV, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.INVALID, Boolean.TRUE, SC_400 });
// Range header without If-Range
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.ALL, null, null, null,
null, null, Boolean.TRUE, SC_206 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.EXACTLY, null, null,
null, null, null, Boolean.TRUE, useStrongEtag.booleanValue() ? SC_206 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.IN, null, null, null,
null, null, Boolean.TRUE, useStrongEtag.booleanValue() ? SC_206 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.NOT_IN, null, null,
null, null, null, Boolean.TRUE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, EtagPrecondition.INVALID, null, null,
null, null, null, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML,
EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, null, null, null, null, Boolean.TRUE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.EQ, null, null,
null, null, Boolean.TRUE, SC_206 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.LT, null, null,
null, null, Boolean.TRUE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.GT, null, null,
null, null, Boolean.TRUE, SC_206 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.MULTI_IN, null,
null, null, null, Boolean.TRUE, SC_206 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.MULTI_IN_REV,
null, null, null, null, Boolean.TRUE, SC_206 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, DatePrecondition.INVALID, null,
null, null, null, Boolean.TRUE, SC_206 });
// If-Range header without Range
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.ALL, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.EXACTLY, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.IN, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.NOT_IN, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.INVALID, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null,
EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.EQ, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.LT, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.GT, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.MULTI_IN, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.MULTI_IN_REV, Boolean.FALSE, SC_200 });
parameterSets.add(new Object[] { useStrongEtag, Task.GET_INDEX_HTML, null, null, null, null, null,
DatePrecondition.INVALID, Boolean.FALSE, SC_200 });
// PUT tests
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, null, null, null, null, null, null,
Boolean.FALSE, SC_204 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.ALL, null, null, null,
null, null, Boolean.FALSE, SC_204 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.EXACTLY, null, null,
null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.IN, null, null, null,
null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.NOT_IN, null, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.INVALID, null, null,
null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_EXIST_TXT, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
null, null, null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, null, null, null, null, null, null,
Boolean.FALSE, SC_201 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.ALL, null, null, null,
null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.IN, null, null, null,
null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.NOT_IN, null, null, null,
null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.INVALID, null, null,
null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.PUT_NEW_TXT, EtagPrecondition.INVALID_ALL_PLUS_OTHER,
null, null, null, null, null, Boolean.FALSE, SC_400 });
// DELETE TESTS
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, null, null, null, null, null, null,
Boolean.FALSE, SC_204 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.ALL, null, null,
null, null, null, Boolean.FALSE, SC_204 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.EXACTLY, null, null,
null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.IN, null, null,
null, null, null, Boolean.FALSE, useStrongEtag.booleanValue() ? SC_204 : SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.NOT_IN, null, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT, EtagPrecondition.INVALID, null, null,
null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_EXIST_TXT,
EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, null, null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, null, null, null, null, null,
null, Boolean.FALSE, SC_404 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.ALL, null, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.IN, null, null,
null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.NOT_IN, null,
null, null, null, null, Boolean.FALSE, SC_412 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT, EtagPrecondition.INVALID, null,
null, null, null, null, Boolean.FALSE, SC_400 });
parameterSets.add(new Object[] { useStrongEtag, Task.DELETE_NOT_EXIST_TXT,
EtagPrecondition.INVALID_ALL_PLUS_OTHER, null, null, null, null, null, Boolean.FALSE, SC_400 });
}
return parameterSets;
}
private static Integer SC_200 = Integer.valueOf(HttpServletResponse.SC_OK);
private static Integer SC_201 = Integer.valueOf(HttpServletResponse.SC_CREATED);
private static Integer SC_204 = Integer.valueOf(HttpServletResponse.SC_NO_CONTENT);
private static Integer SC_206 = Integer.valueOf(HttpServletResponse.SC_PARTIAL_CONTENT);
private static Integer SC_304 = Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED);
private static Integer SC_400 = Integer.valueOf(HttpServletResponse.SC_BAD_REQUEST);
private static Integer SC_404 = Integer.valueOf(HttpServletResponse.SC_NOT_FOUND);
private static Integer SC_412 = Integer.valueOf(HttpServletResponse.SC_PRECONDITION_FAILED);
private enum HTTP_METHOD {
GET,
PUT,
DELETE,
POST,
HEAD
}
private 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_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;
}
}
private enum EtagPrecondition {
EXACTLY,
IN,
ALL,
NOT_IN,
INVALID,
INVALID_ALL_PLUS_OTHER
}
private enum DatePrecondition {
/**
* Condition header value of http date is equivalent to actual resource lastModified date
*/
EQ,
/**
* Condition header value of http date is greater(later) than actual resource lastModified date
*/
GT,
/**
* Condition header value of http date is less(earlier) than actual resource lastModified date
*/
LT,
MULTI_IN,
MULTI_IN_REV,
/**
* not a valid HTTP-date
*/
INVALID,
/**
* None.
*/
NONE;
}
protected void genETagPrecondition(String strongETag, String weakETag, EtagPrecondition condition,
String headerName, Map<String,List<String>> requestHeaders) {
if (condition == null) {
return;
}
List<String> headerValues = new ArrayList<>();
switch (condition) {
case ALL:
headerValues.add("*");
break;
case EXACTLY:
if (strongETag != null) {
headerValues.add(strongETag);
} else {
// Should not happen
throw new IllegalArgumentException("strong etag not found!");
}
break;
case IN:
headerValues.add("\"1a2b3c4d\"");
headerValues.add((weakETag != null ? weakETag + "," : "") +
(strongETag != null ? strongETag + "," : "") + "W/\"*\"");
headerValues.add("\"abcdefg\"");
break;
case NOT_IN:
headerValues.add("\"1a2b3c4d\"");
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));
}
headerValues.add("\"abcdefg\"");
break;
case INVALID:
headerValues.add("invalid-no-quotes");
break;
case INVALID_ALL_PLUS_OTHER:
headerValues.add("*");
headerValues.add("W/\"1abcd\"");
break;
}
if (!headerValues.isEmpty()) {
requestHeaders.put(headerName, headerValues);
}
}
protected void genDatePrecondition(long lastModifiedTimestamp, DatePrecondition condition, String headerName,
Map<String,List<String>> requestHeaders) {
if (condition == null || lastModifiedTimestamp <= 0) {
return;
}
List<String> headerValues = new ArrayList<>();
switch (condition) {
case EQ:
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
break;
case GT:
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
break;
case LT:
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
break;
case MULTI_IN:
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
break;
case MULTI_IN_REV:
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
break;
case INVALID:
headerValues.add("2024.12.09 GMT");
break;
case NONE:
// NO-OP
break;
}
if (!headerValues.isEmpty()) {
requestHeaders.put(headerName, headerValues);
}
}
protected void addPreconditionHeaders(Map<String,List<String>> requestHeaders, String resourceETag,
long lastModified) {
String weakETag = resourceETag;
String strongETag = resourceETag;
if (resourceETag != null) {
if (resourceETag.startsWith("W/")) {
strongETag = resourceETag.substring(2);
} else {
weakETag = "W/" + resourceETag;
}
}
genETagPrecondition(strongETag, weakETag, ifMatchPrecondition, "If-Match", requestHeaders);
genDatePrecondition(lastModified, ifUnmodifiedSincePrecondition, "If-Unmodified-Since", requestHeaders);
genETagPrecondition(strongETag, weakETag, ifNoneMatchPrecondition, "If-None-Match", requestHeaders);
genDatePrecondition(lastModified, ifModifiedSincePrecondition, "If-Modified-Since", requestHeaders);
genETagPrecondition(strongETag, weakETag, ifRangeEtagPrecondition, "If-Range", requestHeaders);
genDatePrecondition(lastModified, ifRangeDatePrecondition, "If-Range", requestHeaders);
}
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);
if (task.m.equals(HTTP_METHOD.PUT)) {
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);
}
if (task.m.equals(HTTP_METHOD.DELETE)) {
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);
}
}
@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", "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);
}
}
addPreconditionHeaders(requestHeaders, etag, lastModified);
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 (addRangeHeader) {
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 = scExpected.intValue() == sc;
Assert.assertTrue("Failure - sc expected:" + String.valueOf(scExpected) + ", sc actual:" + String.valueOf(sc) + ", task:" +
task + ", target resource:(" + etag + "," + FastHttpDateFormat.formatDate(lastModified) +
"), req headers: " + requestHeaders.toString() + ", resp headers: " + responseHeaders.toString(), test);
}
}