Skip to content

Commit

Permalink
Cache request body as soon as possible in case somebody reads it befo…
Browse files Browse the repository at this point in the history
…re vinz
  • Loading branch information
domivds committed Apr 7, 2023
1 parent ac876c3 commit 70c5e7e
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 103 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
id 'java'
id 'java-library'
id 'maven-publish'
id "io.freefair.lombok" version "6.5.1"
}

scmVersion {
Expand Down Expand Up @@ -43,6 +44,9 @@ repositories {
}

dependencies {
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'

implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

Expand Down
9 changes: 9 additions & 0 deletions lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# config can be in parent dir (I think this is already default)
config.stopBubbling=false

# setters return this
lombok.accessors.chain=true

lombok.copyableAnnotations += javax.inject.Named
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
188 changes: 188 additions & 0 deletions src/main/java/cogni/zone/vinzclortho/CacheBodyFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package cogni.zone.vinzclortho;

import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

@RequiredArgsConstructor
public class CacheBodyFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(CacheBodyFilter.class);

public static final String bodyContentProviderAttributeKey = "cogni.zone.vinzclortho.CacheBodyFilter.bodyContentProvider";
public static final String bodySizeProviderAttributeKey = "cogni.zone.vinzclortho.CacheBodyFilter.bodySizeProvider";

private static final byte[] emptyByteArray = new byte[0];
private static final int maxBytesInMemory = 1024 << 10; //1MB (I just alt-entered...)

private final RouteConfigurationService routeConfigurationService;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (routeConfigurationService.hasMatchingRoute(httpRequest)) {
//We patch the request because vinz is nice and lets everybody pass before him
// But not everybody is as nice as vinz, and it might be that the body of the request is already consumed when we get to vinz (foei!)
log.debug("Vinz matches, patching request");
doPatchFilter(httpRequest, response, chain);
}
else {
log.debug("No vinz");
chain.doFilter(request, response);
}
}
private void doPatchFilter(HttpServletRequest httpRequest, ServletResponse response, FilterChain chain) throws ServletException, IOException {
Runnable cleanupCall = null;
InputStream wrappedBodyInputStream;
Supplier<InputStream> cachedBodyInputStreamProvider;
int contentLength = httpRequest.getContentLength();
long realContentLength;
if (-1 == contentLength || contentLength > maxBytesInMemory) {
List<InputStream> toClose = new ArrayList<>();
File tempFile = File.createTempFile("vinzClortho", "bodyCache.dat");
FileUtils.copyToFile(httpRequest.getInputStream(), tempFile);
cleanupCall = () -> cleanup(toClose, tempFile);
cachedBodyInputStreamProvider = () -> getInputStreamForFile(toClose, tempFile);
wrappedBodyInputStream = getInputStreamForFile(toClose, tempFile);
realContentLength = tempFile.length();
}
else {
byte[] bytes = 0 == contentLength ? emptyByteArray : IOUtils.toByteArray(httpRequest.getInputStream());
cachedBodyInputStreamProvider = () -> new ByteArrayInputStream(bytes);
wrappedBodyInputStream = new ByteArrayInputStream(bytes);
realContentLength = bytes.length;
}

httpRequest.setAttribute(bodyContentProviderAttributeKey, cachedBodyInputStreamProvider);
httpRequest.setAttribute(bodySizeProviderAttributeKey, realContentLength);
HttpServletRequest wrapper = new VinzServletRequestWrapper(httpRequest, new BodyInputStream(wrappedBodyInputStream));

try {
chain.doFilter(wrapper, response);
}
finally {
if (null != cleanupCall) cleanupCall.run();
}
}

private InputStream getInputStreamForFile(List<InputStream> toClose, File tempFile) {
try {
InputStream inputStream = new FileInputStream(tempFile);
toClose.add(inputStream);
return inputStream;
}
catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}

private void cleanup(List<InputStream> inputStreams, File file) {
inputStreams.forEach(IOUtils::closeQuietly);
if (!file.delete()) file.deleteOnExit();
}

private static class VinzServletRequestWrapper extends HttpServletRequestWrapper {
private final BodyInputStream bodyInputStream;

private VinzServletRequestWrapper(HttpServletRequest request, BodyInputStream bodyInputStream) {
super(request);
this.bodyInputStream = bodyInputStream;
}

@Override
public ServletInputStream getInputStream() throws IOException {
return bodyInputStream;
}
}

private static class BodyInputStream extends ServletInputStream {

private final InputStream inputStream;

private BodyInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException("Nop");
}

@Override
public int read() throws IOException {
return inputStream.read();
}

@Override
public int read(byte[] bytes, int off, int len) throws IOException {
return inputStream.read(bytes, off, len);
}

@Override
public int read(byte[] bytes) throws IOException {
return inputStream.read(bytes);
}

@Override
public long skip(long byteCount) throws IOException {
return inputStream.skip(byteCount);
}

@Override
public int available() throws IOException {
return inputStream.available();
}

@Override
public void close() throws IOException {
inputStream.close();
}

@Override
public synchronized void mark(int readlimit) {
inputStream.mark(readlimit);
}

@Override
public synchronized void reset() throws IOException {
inputStream.reset();
}

@Override
public boolean markSupported() {
return inputStream.markSupported();
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cogni.zone.vinzclortho;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Slf4j
public class RouteConfigurationService {

private final Configuration config;

public boolean hasMatchingRoute(HttpServletRequest httpRequest) {
return findRoute(httpRequest).isPresent();
}

public Optional<Route> findRoute(HttpServletRequest httpRequest) {
return config.getRoutes().stream()
.filter(route -> matches(route, httpRequest))
.findFirst();
}

private boolean matches(Route route, HttpServletRequest request) {
return new AntPathMatcher().match(route.getPath(), request.getServletPath());
}

@Data
public static class Configuration {
private List<Route> routes = Collections.synchronizedList(new ArrayList<>());
}

@Data
public static class Route {
private String name;
private String path;
private String url;
}
}
28 changes: 21 additions & 7 deletions src/main/java/cogni/zone/vinzclortho/VinzClorthoConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,35 @@
@Configuration
@EnableConfigurationProperties
public class VinzClorthoConfiguration {
public static final int filterOrder = Ordered.LOWEST_PRECEDENCE;

@Bean
@ConfigurationProperties(prefix = "cognizone.vinz")
public VinzClorthoFilter.Configuration vinzClorthoConfig() {
return new VinzClorthoFilter.Configuration();
public RouteConfigurationService.Configuration vinzClorthoConfig() {
return new RouteConfigurationService.Configuration();
}

@Bean
public RouteConfigurationService routeConfigurationService() {
return new RouteConfigurationService(vinzClorthoConfig());
}

@Bean
@Lazy(false)
public FilterRegistrationBean<Filter> vinzClorthoMainFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new VinzClorthoFilter(routeConfigurationService()));
filterFilterRegistrationBean.setOrder(Ordered.LOWEST_PRECEDENCE);
filterFilterRegistrationBean.setName("vinzClorthoMainFilter");
return filterFilterRegistrationBean;
}

@Bean
@Lazy(false)
public FilterRegistrationBean<Filter> vinzClorthoFilter() {
public FilterRegistrationBean<Filter> vinzClorthoCacheBodyFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new VinzClorthoFilter(vinzClorthoConfig()));
filterFilterRegistrationBean.setOrder(filterOrder);
filterFilterRegistrationBean.setName("vinzClorthoFilter");
filterFilterRegistrationBean.setFilter(new CacheBodyFilter(routeConfigurationService()));
filterFilterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
filterFilterRegistrationBean.setName("vinzClorthoCacheBodyFilter");
return filterFilterRegistrationBean;
}

Expand Down
Loading

0 comments on commit 70c5e7e

Please sign in to comment.