From 58b4905c91948867176c01302d7621a0a7023a68 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 11 Jan 2016 18:43:57 +0100 Subject: [PATCH] apply an idle timeout to chunked put requests (as finder doesn't terminate chunked transfers properly) [ci skip] --- .../webdav/filters/PutIdleTimeoutFilter.java | 128 ++++++++++++++++++ .../FileSystemBasedWebDavServer.java | 4 +- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java new file mode 100644 index 000000000..39b13b420 --- /dev/null +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java @@ -0,0 +1,128 @@ +package org.cryptomator.webdav.filters; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +/** + * Wrapps the {@link ServletInputStream} into a timeout-aware stream, that returns EOF after a certain timeout. + * Wrapping is done only for chunked PUT requests, as some WebDAV clients are too stupid to send a EOF-chunk (0-byte-chunk). + */ +public class PutIdleTimeoutFilter implements HttpFilter { + + private static final long TIMEOUT = 100; + private static final TimeUnit TIMEOUT_UNIT = TimeUnit.MILLISECONDS; + private static final String METHOD_PUT = "PUT"; + private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding"; + private static final String HEADER_TRANSFER_ENCODING_CHUNKED = "chunked"; + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no-op + } + + @Override + public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + if (METHOD_PUT.equalsIgnoreCase(request.getMethod()) && HEADER_TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(request.getHeader(HEADER_TRANSFER_ENCODING))) { + chain.doFilter(new PutRequestWithIdleTimeout(request), response); + } else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + executor.shutdownNow(); + } + + private class PutRequestWithIdleTimeout extends HttpServletRequestWrapper { + + public PutRequestWithIdleTimeout(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return new IdleTimeoutServletInputStream(super.getInputStream()); + } + + } + + private class IdleTimeoutServletInputStream extends ServletInputStream { + + private final ServletInputStream delegate; + private boolean timedOut = false; + + public IdleTimeoutServletInputStream(ServletInputStream delegate) { + this.delegate = delegate; + } + + @Override + public boolean isFinished() { + return timedOut || delegate.isFinished(); + } + + @Override + public boolean isReady() { + return !timedOut && delegate.isReady(); + } + + @Override + public void setReadListener(ReadListener readListener) { + delegate.setReadListener(readListener); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + Future readTask = executor.submit(() -> { + return delegate.read(b, off, len); + }); + return readTask.get(TIMEOUT, TIMEOUT_UNIT); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } catch (ExecutionException e) { + throw new IOException("Exception during read", e); + } catch (TimeoutException e) { + timedOut = true; + return -1; + } + } + + @Override + public int read() throws IOException { + try { + Future readTask = executor.submit(() -> { + return delegate.read(); + }); + return readTask.get(TIMEOUT, TIMEOUT_UNIT); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } catch (ExecutionException e) { + throw new IOException("Exception during read", e); + } catch (TimeoutException e) { + // throw new InterruptedByTimeoutException(); + timedOut = true; + return -1; + } + } + + } + +} diff --git a/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java index 9081b6fe2..f8defc352 100644 --- a/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java +++ b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java @@ -19,6 +19,7 @@ import javax.servlet.DispatcherType; import org.cryptomator.filesystem.FileSystem; import org.cryptomator.webdav.filters.AcceptRangeFilter; import org.cryptomator.webdav.filters.LoggingHttpFilter; +import org.cryptomator.webdav.filters.PutIdleTimeoutFilter; import org.cryptomator.webdav.filters.UriNormalizationFilter; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; @@ -55,10 +56,11 @@ class FileSystemBasedWebDavServer { servletContext.addServlet(servletHolder, "/*"); servletContext.addFilter(AcceptRangeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(UriNormalizationFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + servletContext.addFilter(PutIdleTimeoutFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(LoggingHttpFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); servletCollection.mapContexts(); - server.setConnectors(new Connector[] { localConnector }); + server.setConnectors(new Connector[] {localConnector}); server.setHandler(servletCollection); }