diff --git a/core/src/main/java/com/google/zxing/MultiFormatReader.java b/core/src/main/java/com/google/zxing/MultiFormatReader.java index af1d23f55..581472932 100644 --- a/core/src/main/java/com/google/zxing/MultiFormatReader.java +++ b/core/src/main/java/com/google/zxing/MultiFormatReader.java @@ -169,6 +169,9 @@ public final class MultiFormatReader implements Reader { private Result decodeInternal(BinaryBitmap image) throws NotFoundException { if (readers != null) { for (Reader reader : readers) { + if (Thread.currentThread().isInterrupted()) { + throw NotFoundException.getNotFoundInstance(); + } try { return reader.decode(image, hints); } catch (ReaderException re) { @@ -179,6 +182,9 @@ public final class MultiFormatReader implements Reader { // Calling all readers again with inverted image image.getBlackMatrix().flip(); for (Reader reader : readers) { + if (Thread.currentThread().isInterrupted()) { + throw NotFoundException.getNotFoundInstance(); + } try { return reader.decode(image, hints); } catch (ReaderException re) { diff --git a/zxingorg/src/main/java/com/google/zxing/web/DecodeServlet.java b/zxingorg/src/main/java/com/google/zxing/web/DecodeServlet.java index 45b23b8f1..f0a948b30 100644 --- a/zxingorg/src/main/java/com/google/zxing/web/DecodeServlet.java +++ b/zxingorg/src/main/java/com/google/zxing/web/DecodeServlet.java @@ -391,7 +391,7 @@ public final class DecodeServlet extends HttpServlet { savedException = re; } - if (results.isEmpty()) { + if (results.isEmpty() && !Thread.currentThread().isInterrupted()) { try { // Look for pure barcode Result theResult = reader.decode(bitmap, HINTS_PURE); @@ -403,7 +403,7 @@ public final class DecodeServlet extends HttpServlet { } } - if (results.isEmpty()) { + if (results.isEmpty() && !Thread.currentThread().isInterrupted()) { try { // Look for normal barcode in photo Result theResult = reader.decode(bitmap, HINTS); @@ -415,7 +415,7 @@ public final class DecodeServlet extends HttpServlet { } } - if (results.isEmpty()) { + if (results.isEmpty() && !Thread.currentThread().isInterrupted()) { try { // Try again with other binarizer BinaryBitmap hybridBitmap = new BinaryBitmap(new HybridBinarizer(source)); diff --git a/zxingorg/src/main/java/com/google/zxing/web/TimeoutFilter.java b/zxingorg/src/main/java/com/google/zxing/web/TimeoutFilter.java new file mode 100644 index 000000000..ed11b5694 --- /dev/null +++ b/zxingorg/src/main/java/com/google/zxing/web/TimeoutFilter.java @@ -0,0 +1,92 @@ +/* + * Copyright 2022 ZXing authors + * + * Licensed 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 com.google.zxing.web; + +import com.google.common.util.concurrent.SimpleTimeLimiter; +import com.google.common.util.concurrent.TimeLimiter; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.annotation.WebFilter; +import javax.servlet.annotation.WebInitParam; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; + +/** + * Protect the decode endpoint from long-running requests. + */ +@WebFilter(urlPatterns = {"/w/decode"}, initParams = { + @WebInitParam(name = "timeoutSec", value = "10"), +}) +public final class TimeoutFilter implements Filter { + + private ExecutorService executorService; + private TimeLimiter timeLimiter; + private int timeoutSec; + + @Override + public void init(FilterConfig filterConfig) { + executorService = Executors.newCachedThreadPool(); + timeLimiter = SimpleTimeLimiter.create(executorService); + timeoutSec = Integer.parseInt(filterConfig.getInitParameter("timeoutSec")); + } + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) throws IOException, ServletException { + try { + timeLimiter.callWithTimeout(new Callable() { + @Override + public Void call() throws Exception { + chain.doFilter(request, response); + return null; + } + }, timeoutSec, TimeUnit.SECONDS); + } catch (TimeoutException | InterruptedException e) { + HttpServletResponse servletResponse = (HttpServletResponse) response; + servletResponse.setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT); + servletResponse.getWriter().write("Request took too long"); + } catch (ExecutionException e) { + if (e.getCause() instanceof ServletException) { + throw (ServletException) e.getCause(); + } + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } + throw new ServletException(e.getCause()); + } + } + + @Override + public void destroy() { + if (executorService != null) { + executorService.shutdownNow(); + } + } + +} diff --git a/zxingorg/src/test/java/com/google/zxing/web/TimeoutFilterTestCase.java b/zxingorg/src/test/java/com/google/zxing/web/TimeoutFilterTestCase.java new file mode 100644 index 000000000..3087b3408 --- /dev/null +++ b/zxingorg/src/test/java/com/google/zxing/web/TimeoutFilterTestCase.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 ZXing authors + * + * Licensed 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 com.google.zxing.web; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.GenericServlet; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Tests {@link TimeoutFilter}. + */ +public final class TimeoutFilterTestCase extends Assert { + + @Test + public void testTimeout() throws Exception { + MockFilterConfig config = new MockFilterConfig(); + config.addInitParameter("timeoutSec", "1"); + Filter filter = new TimeoutFilter(); + filter.init(config); + + FilterChain chain = new MockFilterChain(new GenericServlet() { + @Override + public void service(ServletRequest req, ServletResponse res) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + // continue + } + } + }); + HttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(new MockHttpServletRequest(), response, chain); + filter.destroy(); + assertEquals(HttpServletResponse.SC_REQUEST_TIMEOUT, response.getStatus()); + } + +}