package io.loli.kaze.cache; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import javax.cache.Cache; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.littleshoot.proxy.HttpFiltersAdapter; public class CacheFilterAdapter extends HttpFiltersAdapter { private static final Logger logger = Logger .getLogger(CacheFilterAdapter.class); private Cache<String, KazeCacheData> cache; private String cacheRegex; public CacheFilterAdapter(HttpRequest originalRequest, ChannelHandlerContext ctx, String cacheRegex) { super(originalRequest, ctx); this.cacheRegex = cacheRegex; cache = KazeCacheFactory.getCache(CacheFilter.class.getName()); } @Override public HttpResponse requestPre(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { String method = originalRequest.getMethod().toString(); if ("GET".equals(method)) { String url = originalRequest.getUri(); String allString = url; if (allString.matches(cacheRegex)) { String key = method + allString; KazeCacheData data = cache.get(DigestUtils.md5Hex(key)); // if status is 200 or 304 if (data != null) { if (isRequestCacheable(originalRequest)) { ByteBuf buf = Unpooled.copiedBuffer(data.getData()); DefaultFullHttpResponse resp = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(200)); resp.content().writeBytes(buf); buf.release(); data.getHeaders() .entrySet() .forEach( entry -> { resp.headers().add( entry.getKey(), entry.getValue()); }); return resp; } } } } } return null; } @Override public HttpResponse requestPost(HttpObject httpObject) { return null; } // @Override // public HttpResponse requestPost(HttpObject httpObject) { // ChannelPipeline pipeline = ctx.pipeline(); // pipeline.addLast(new HttpServerCodec(409600, 8192, 8192000)); // return super.requestPost(httpObject); // } private ThreadLocal<KazeCacheData> context = new ThreadLocal<>(); @Override public HttpObject responsePre(HttpObject httpObject) { String method = originalRequest.getMethod().toString(); String url = originalRequest.getUri(); String key = method + url; if (url.matches(cacheRegex) && "GET".equals(method)) { if (httpObject instanceof HttpResponse) { HttpResponse resp = (HttpResponse) httpObject; if (isCacheable(originalRequest, resp)) { KazeCacheData data = new KazeCacheData(); data.setContentType(resp.headers().get("Content-Type")); data.setStatus(resp.getStatus().code()); data.setHeaders(resp .headers() .entries() .stream() .collect( Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()))); context.set(data); } } if (httpObject instanceof HttpContent) { KazeCacheData data = context.get(); if (data != null) { HttpContent content = (HttpContent) httpObject; ByteBuf copy = content.content().duplicate(); ByteBufToBytes reader = new ByteBufToBytes( copy.readableBytes()); reader.reading(copy); if (data.getData() == null) { data.setData(reader.readFull()); } else { data.setData(ArrayUtils.addAll(data.getData(), reader.readFull())); } if (httpObject instanceof LastHttpContent) { cache.put(DigestUtils.md5Hex(key), data); context.remove(); } } } } return httpObject; } @Override public HttpObject responsePost(HttpObject httpObject) { return httpObject; } private boolean isRequestCacheable(final HttpRequest httpRequest) { final String requestControl = httpRequest.headers().get( HttpHeaders.Names.CACHE_CONTROL); final Set<String> cacheControl = new HashSet<String>(); cacheControl.add(requestControl); // We should really follow all the caching rules from: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html // // The effect is not caching some things we could. if (!cacheControl.isEmpty()) { if (cacheControl.contains(HttpHeaders.Values.NO_CACHE)) { logger.info("No cache header"); return false; } if (cacheControl.contains(HttpHeaders.Values.PRIVATE)) { logger.info("Private header"); return false; } if (cacheControl.contains(HttpHeaders.Values.NO_STORE)) { logger.info("No store header"); return false; } if (cacheControl.contains(HttpHeaders.Values.MUST_REVALIDATE)) { logger.info("Not caching with 'must revalidate' header"); return false; } if (cacheControl.contains(HttpHeaders.Values.PROXY_REVALIDATE)) { logger.info("Not caching with 'proxy revalidate' header"); return false; } } return true; } private boolean isCacheable(final HttpRequest httpRequest, final HttpResponse httpResponse) { final HttpResponseStatus responseStatus = httpResponse.getStatus(); final boolean cachableStatus; final int status = responseStatus.code(); // For rules on this, see: // http://tools.ietf.org/html/rfc2616#section-13.4 // // We can't cache 206 responses unless we can support the Range // header in requests. That would be a fantastic extension. switch (status) { case 200: case 203: case 300: case 301: case 410: cachableStatus = true; break; default: cachableStatus = false; break; } if (!cachableStatus) { logger.info("HTTP status is not cachable:" + status); return false; } // Don't use the cache if the request has cookies -- security violation. if (httpResponse.headers().contains(HttpHeaders.Names.SET_COOKIE)) { logger.info("Response contains set cookie header"); return false; } if (httpResponse.headers().contains(HttpHeaders.Names.SET_COOKIE2)) { logger.info("Response contains set cookie2 header"); return false; } /* * if (httpRequest.containsHeader(HttpHeaders.Names.COOKIE)) { * logger.info("Request contains Cookie header"); return false; } */ final String responseControl = httpResponse.headers().get( HttpHeaders.Names.CACHE_CONTROL); final String requestControl = httpRequest.headers().get( HttpHeaders.Names.CACHE_CONTROL); final Set<String> cacheControl = new HashSet<String>(); cacheControl.add(requestControl); cacheControl.add(responseControl); // We should really follow all the caching rules from: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html // // The effect is not caching some things we could. if (!cacheControl.isEmpty()) { if (cacheControl.contains(HttpHeaders.Values.NO_CACHE)) { logger.info("No cache header"); return false; } if (cacheControl.contains(HttpHeaders.Values.PRIVATE)) { logger.info("Private header"); return false; } if (cacheControl.contains(HttpHeaders.Values.NO_STORE)) { logger.info("No store header"); return false; } if (cacheControl.contains(HttpHeaders.Values.MUST_REVALIDATE)) { logger.info("Not caching with 'must revalidate' header"); return false; } if (cacheControl.contains(HttpHeaders.Values.PROXY_REVALIDATE)) { logger.info("Not caching with 'proxy revalidate' header"); return false; } } if (httpResponse != null) { final String responsePragma = httpResponse.headers().get( HttpHeaders.Names.PRAGMA); if (StringUtils.isNotBlank(responsePragma) && responsePragma.contains(HttpHeaders.Values.NO_CACHE)) { logger.info("Not caching with response pragma no cache"); return false; } } final String requestPragma = httpRequest.headers().get( HttpHeaders.Names.PRAGMA); if (StringUtils.isNotBlank(requestPragma) && requestPragma.contains(HttpHeaders.Values.NO_CACHE)) { logger.info("Not caching with request pragma no cache"); return false; } logger.info("Got cachable response!"); return true; } }