CacheUtils.java
/*
* Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.synapse.transport.netty.util;
import org.wso2.transport.http.netty.message.HttpCarbonMessage;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
/**
* A utility class for HTTP caching related tasks.
*/
public class CacheUtils {
public static final String WEAK_VALIDATOR_TAG = "W/";
public static final String ETAG_HEADER = "ETag";
public static final String IF_NONE_MATCH_HEADER = "If-None-Match";
public static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since";
public static final String LAST_MODIFIED_HEADER = "Last-Modified";
/**
* Method for revalidating a cached response. This method follows the RFC7232 and RFC7234 specifications.
*
* @param outboundResponse The response to be sent to downstream
* @param inboundRequest The request received from downstream
* @return Returns true if the cached response is still valid
*/
public static boolean isValidCachedResponse(HttpCarbonMessage outboundResponse, HttpCarbonMessage inboundRequest) {
String outgoingETag = outboundResponse.getHeader(ETAG_HEADER);
String incomingETags = inboundRequest.getHeader(IF_NONE_MATCH_HEADER);
if (incomingETags != null) {
if (outgoingETag == null) {
// If inbound request has the If-None-Match header, but the outgoing request does not have an ETag
// header, it is considered that the cached response is invalid.
return false;
}
return !isNonMatchingETag(incomingETags, outgoingETag);
}
// If there isn't an If-None-Match header, then check if there is a If-Modified-Since header.
String ifModifiedSince = inboundRequest.getHeader(IF_MODIFIED_SINCE_HEADER);
if (ifModifiedSince == null) {
// If both If-None-Match and If-Modified-Since headers aren't there, then it is not looking for cache
// revalidation.
return false;
}
String lastModified = outboundResponse.getHeader(LAST_MODIFIED_HEADER);
if (lastModified == null) {
return false;
}
try {
TemporalAccessor ifModifiedSinceTime = ZonedDateTime.parse(ifModifiedSince,
DateTimeFormatter.RFC_1123_DATE_TIME);
TemporalAccessor lastModifiedTime = ZonedDateTime.parse(lastModified, DateTimeFormatter.RFC_1123_DATE_TIME);
return ifModifiedSinceTime.equals(lastModifiedTime);
} catch (DateTimeParseException e) {
// If the Date header cannot be parsed, it is ignored.
return false;
}
}
private static boolean isNonMatchingETag(String etags, String outgoingETag) {
String[] etagArray = etags.split(",");
if (etagArray.length == 1 && "*".equals(etagArray[0])) {
return false;
}
for (String etag : etagArray) {
if (weakEquals(etag.trim(), outgoingETag)) {
return false;
}
}
return true;
}
private static boolean weakEquals(String requestETag, String responseETag) {
// Taking the sub string to ignore "W/"
String requestTagPortion = isWeakEntityTag(requestETag) ?
requestETag.substring(WEAK_VALIDATOR_TAG.length()) : requestETag;
String responseTagPortion = isWeakEntityTag(responseETag) ?
responseETag.substring(WEAK_VALIDATOR_TAG.length()) : responseETag;
return requestTagPortion.equals(responseTagPortion);
}
private static boolean isWeakEntityTag(String etag) {
return etag.startsWith(WEAK_VALIDATOR_TAG);
}
private CacheUtils() {
}
}