Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wzy1935 committed Jul 24, 2024
1 parent 5c8fd99 commit 0fdc2d6
Show file tree
Hide file tree
Showing 11 changed files with 757 additions and 85 deletions.
12 changes: 8 additions & 4 deletions src/main/java/io/vertx/httpproxy/impl/CacheControl.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public CacheControl parse(String header) {
_private = false;
proxyRevalidate = false;
_public = false;
maxAge = -1;
maxStale = -1;
minFresh = -1;
sMaxage = -1;

String[] parts = header.split(","); // No regex
for (String part : parts) {
Expand Down Expand Up @@ -74,10 +78,10 @@ public CacheControl parse(String header) {
proxyRevalidate = true;
break;
default:
maxAge = loadInt(part, "max-age=");
maxStale = loadInt(part, "max-stale=");
minFresh = loadInt(part, "min-fresh=");
sMaxage = loadInt(part, "s-maxage=");
maxAge = Math.max(maxAge, loadInt(part, "max-age="));
maxStale = Math.max(maxStale, loadInt(part, "max-stale="));
minFresh = Math.max(minFresh, loadInt(part, "min-fresh="));
sMaxage = Math.max(sMaxage, loadInt(part, "s-maxage="));
break;
}
}
Expand Down
142 changes: 82 additions & 60 deletions src/main/java/io/vertx/httpproxy/impl/CachingFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
import io.vertx.httpproxy.spi.cache.Resource;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

class CachingFilter implements ProxyInterceptor {

private static final String SKIP_CACHE_RESPONSE_HANDLING = "skip_cache_response_handling";
private static final String CACHED_RESOURCE = "cached_resource";

private final Cache cache;

public CachingFilter(Cache cache) {
Expand All @@ -32,14 +36,19 @@ public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {

@Override
public Future<Void> handleProxyResponse(ProxyContext context) {
return sendAndTryCacheProxyResponse(context);
Boolean skip = context.get(SKIP_CACHE_RESPONSE_HANDLING, Boolean.class);
if (skip != null && skip) {
return context.sendResponse();
} else {
return sendAndTryCacheProxyResponse(context);
}
}

private Future<Void> sendAndTryCacheProxyResponse(ProxyContext context) {

ProxyResponse response = context.response();
ProxyRequest request = response.request();
Resource cached = context.get("cached_resource", Resource.class);
Resource cached = context.get(CACHED_RESOURCE, Resource.class);
String absoluteUri = request.absoluteURI();

if (cached != null && response.getStatusCode() == 304) {
Expand All @@ -65,7 +74,7 @@ private Future<Void> sendAndTryCacheProxyResponse(ProxyContext context) {
canCache = false;
}
}
if (response.headers().get(HttpHeaders.AUTHORIZATION) != null) {
if (request.headers().get(HttpHeaders.AUTHORIZATION) != null) {
if (
responseCacheControl == null || (
!responseCacheControl.isMustRevalidate()
Expand All @@ -78,6 +87,9 @@ private Future<Void> sendAndTryCacheProxyResponse(ProxyContext context) {
if (requestCacheControl != null && requestCacheControl.isNoStore()) {
canCache = false;
}
if ("*".equals(response.headers().get(HttpHeaders.VARY))) {
canCache = false;
}
if (canCache) {
if (request.getMethod() == HttpMethod.GET) {
Resource res = new Resource(
Expand Down Expand Up @@ -114,9 +126,6 @@ private static MultiMap varyHeaders(MultiMap requestHeaders, MultiMap responseHe
MultiMap result = MultiMap.caseInsensitiveMultiMap();
String vary = responseHeaders.get(HttpHeaders.VARY);
if (vary != null) {
if (vary.trim().equals("*")) {
return result.addAll(requestHeaders);
}
for (String toVary : vary.split(",")) {
toVary = toVary.trim();
String toVaryValue = requestHeaders.get(toVary);
Expand Down Expand Up @@ -149,62 +158,22 @@ private Future<ProxyResponse> tryHandleProxyRequestFromCache(ProxyContext contex
return cache.get(cacheKey).compose(resource -> {
if (resource == null || !checkVaryHeaders(proxyRequest.headers(), resource.getRequestVaryHeader())) {
if (requestCacheControl != null && requestCacheControl.isOnlyIfCached()) {
context.set(SKIP_CACHE_RESPONSE_HANDLING, true);
return Future.succeededFuture(proxyRequest.release().response().setStatusCode(504));
}
return context.sendRequest();
}

boolean validInboundCache = false;
String inboundIfModifiedSince = inboundRequest.getHeader(HttpHeaders.IF_MODIFIED_SINCE);
String inboundIfNoneMatch = inboundRequest.getHeader(HttpHeaders.IF_NONE_MATCH);
Instant resourceLastModified = resource.getLastModified();
String resourceETag = resource.getEtag();
if (resource.getStatusCode() == 200) { // TODO: status code 206
if (inboundIfNoneMatch != null && resourceETag != null) {
String[] inboundETags = inboundIfNoneMatch.split(",");
for (String inboundETag : inboundETags) {
inboundETag = inboundETag.trim();
if (inboundETag.equals(resourceETag)) {
validInboundCache = true;
break;
}
}
} else if (inboundIfModifiedSince != null && resourceLastModified != null) {
if (ParseUtils.parseHeaderDate(inboundIfModifiedSince).isAfter(resourceLastModified)) { // TODO: is it wrong???
validInboundCache = true;
}
}
}
if (validInboundCache) {
MultiMap infoHeaders = MultiMap.caseInsensitiveMultiMap();
List<CharSequence> headersNeeded = List.of(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CONTENT_LOCATION,
HttpHeaders.DATE,
HttpHeaders.ETAG,
HttpHeaders.EXPIRES,
HttpHeaders.VARY
);
for (CharSequence header : headersNeeded) {
String value = resource.getHeaders().get(header);
if (value != null) infoHeaders.add(header, value);
}
ProxyResponse resp = proxyRequest.release().response();
resp.headers().setAll(infoHeaders);
resp.setStatusCode(304);
return Future.succeededFuture(resp);
}

// to check if the resource is fresh
boolean needValidate = false;
String resourceCacheControlHeader = resource.getHeaders().get(HttpHeaders.CACHE_CONTROL);
CacheControl resourceCacheControl = resourceCacheControlHeader == null ? null : new CacheControl().parse(resourceCacheControlHeader);
if (resourceCacheControl != null && resourceCacheControl.isNoCache()) needValidate = true;
if (requestCacheControl != null && requestCacheControl.isNoCache()) needValidate = true;
long age = Math.subtractExact(System.currentTimeMillis(), resource.getTimestamp()); // in ms
long maxAge = Math.max(0, resource.getMaxAge());
if (resourceCacheControl != null && (resourceCacheControl.isMustRevalidate() || resourceCacheControl.isProxyRevalidate())) {
if (age > maxAge) needValidate = true;
} else if (requestCacheControl != null) {
boolean responseValidateOverride = resourceCacheControl != null && (resourceCacheControl.isMustRevalidate() || resourceCacheControl.isProxyRevalidate());
if (!responseValidateOverride && requestCacheControl != null) {
if (requestCacheControl.maxAge() != -1) {
maxAge = Math.min(maxAge, SafeMathUtils.safeMultiply(requestCacheControl.maxAge(), 1000));
}
Expand All @@ -213,8 +182,8 @@ private Future<ProxyResponse> tryHandleProxyRequestFromCache(ProxyContext contex
} else if (requestCacheControl.maxStale() != -1) {
maxAge += SafeMathUtils.safeMultiply(requestCacheControl.maxStale(), 1000);
}
if (age > maxAge) needValidate = true;
}
if (age > maxAge) needValidate = true;
String etag = resource.getHeaders().get(HttpHeaders.ETAG);
String lastModified = resource.getHeaders().get(HttpHeaders.LAST_MODIFIED);
if (needValidate) {
Expand All @@ -224,13 +193,68 @@ private Future<ProxyResponse> tryHandleProxyRequestFromCache(ProxyContext contex
if (lastModified != null) {
proxyRequest.headers().set(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
}
context.set("cached_resource", resource);
context.set(CACHED_RESOURCE, resource);
return context.sendRequest();
} else {
proxyRequest.release();
ProxyResponse proxyResponse = proxyRequest.response();
resource.init(proxyResponse, inboundRequest.method() == HttpMethod.GET);
return Future.succeededFuture(proxyResponse);
// check if the client already have valid cache using current cache
boolean validInboundCache = false;
Instant inboundIfModifiedSince = ParseUtils.parseHeaderDate(inboundRequest.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
String inboundIfNoneMatch = inboundRequest.getHeader(HttpHeaders.IF_NONE_MATCH);
Instant resourceLastModified = resource.getLastModified();
Instant resourceDate = ParseUtils.parseHeaderDate(resource.getHeaders().get(HttpHeaders.DATE));
String resourceETag = resource.getEtag();
if (resource.getStatusCode() == 200) {
if (inboundIfNoneMatch != null) {
if (resourceETag != null) {
String[] inboundETags = inboundIfNoneMatch.split(",");
for (String inboundETag : inboundETags) {
inboundETag = inboundETag.trim();
if (inboundETag.equals(resourceETag)) {
validInboundCache = true;
break;
}
}
}
} else if (inboundIfModifiedSince != null) {
if (resourceLastModified != null) {
if (!inboundIfModifiedSince.isBefore(resourceLastModified)) {
validInboundCache = true;
}
} else if (resourceDate != null) {
if (!inboundIfModifiedSince.isBefore(resourceDate)) {
validInboundCache = true;
}
}

}
}
if (validInboundCache) {
MultiMap infoHeaders = MultiMap.caseInsensitiveMultiMap();
List<CharSequence> headersNeeded = new ArrayList<>(List.of(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CONTENT_LOCATION,
HttpHeaders.DATE,
HttpHeaders.ETAG,
HttpHeaders.EXPIRES,
HttpHeaders.VARY
));
if (inboundIfNoneMatch == null) headersNeeded.add(HttpHeaders.LAST_MODIFIED);
for (CharSequence header : headersNeeded) {
String value = resource.getHeaders().get(header);
if (value != null) infoHeaders.add(header, value);
}
ProxyResponse resp = proxyRequest.release().response();
resp.headers().setAll(infoHeaders);
resp.setStatusCode(304);
context.set(SKIP_CACHE_RESPONSE_HANDLING, true);
return Future.succeededFuture(resp);
} else {
proxyRequest.release();
ProxyResponse proxyResponse = proxyRequest.response();
resource.init(proxyResponse, inboundRequest.method() == HttpMethod.GET);
context.set(SKIP_CACHE_RESPONSE_HANDLING, true);
return Future.succeededFuture(proxyResponse);
}
}

});
Expand All @@ -240,11 +264,9 @@ private Future<ProxyResponse> tryHandleProxyRequestFromCache(ProxyContext contex

private static boolean checkVaryHeaders(MultiMap requestHeaders, MultiMap varyHeaders) {
for (Map.Entry<String, String> e: varyHeaders) {
String fromVary = e.getValue().toLowerCase();
String fromVary = e.getValue();
String fromRequest = requestHeaders.get(e.getKey());
if (fromRequest == null) return false;
fromRequest = fromVary.toLowerCase();
if (!fromRequest.equals(fromVary)) return false;
if (fromRequest == null || !fromRequest.equals(fromVary)) return false;
}
return true;
}
Expand Down
36 changes: 17 additions & 19 deletions src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,23 @@ class ProxiedResponse implements ProxyResponse {
long maxAge = -1;
boolean publicCacheControl = false;
String cacheControlHeader = response.getHeader(HttpHeaders.CACHE_CONTROL);
if (cacheControlHeader != null) {
CacheControl cacheControl = new CacheControl().parse(cacheControlHeader);
if (cacheControl.isPublic()) {
publicCacheControl = true;
if (cacheControl.sMaxage() >= 0) {
maxAge = (long) cacheControl.sMaxage() * 1000;
} else if (cacheControl.maxAge() >= 0) {
maxAge = (long) cacheControl.maxAge() * 1000;
} else {
String dateHeader = response.getHeader(HttpHeaders.DATE);
String expiresHeader = response.getHeader(HttpHeaders.EXPIRES);
if (dateHeader != null) {
if (expiresHeader != null) {
maxAge = Math.max(0, ParseUtils.parseHeaderDate(expiresHeader).toEpochMilli() - ParseUtils.parseHeaderDate(dateHeader).toEpochMilli());
} else if (heuristicallyCacheable(response)) {
String lastModifiedHeader = response.getHeader(HttpHeaders.LAST_MODIFIED);
maxAge = Math.max(0, (ParseUtils.parseHeaderDate(lastModifiedHeader).toEpochMilli() - ParseUtils.parseHeaderDate(dateHeader).toEpochMilli()) / 10);
}
}
CacheControl cacheControl = cacheControlHeader == null ? null : new CacheControl().parse(cacheControlHeader);
if (cacheControl != null && cacheControl.isPublic()) {
publicCacheControl = true;
}
if (cacheControl != null && cacheControl.sMaxage() >= 0) {
maxAge = (long) cacheControl.sMaxage() * 1000;
} else if (cacheControl != null && cacheControl.maxAge() >= 0) {
maxAge = (long) cacheControl.maxAge() * 1000;
} else {
String dateHeader = response.getHeader(HttpHeaders.DATE);
String expiresHeader = response.getHeader(HttpHeaders.EXPIRES);
if (dateHeader != null) {
if (expiresHeader != null) {
maxAge = Math.max(0, ParseUtils.parseHeaderDate(expiresHeader).toEpochMilli() - ParseUtils.parseHeaderDate(dateHeader).toEpochMilli());
} else if (heuristicallyCacheable(response)) {
String lastModifiedHeader = response.getHeader(HttpHeaders.LAST_MODIFIED);
maxAge = Math.max(0, (ParseUtils.parseHeaderDate(lastModifiedHeader).toEpochMilli() - ParseUtils.parseHeaderDate(dateHeader).toEpochMilli()) / 10);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module io.vertx.httpproxy {
requires io.netty.codec.http;
requires transitive io.vertx.core;
requires io.vertx.core.logging;
requires static io.vertx.codegen.api;
Expand Down
Loading

0 comments on commit 0fdc2d6

Please sign in to comment.