diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java index 70c64278e24..9c753d5e59b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -37,6 +38,7 @@ import java.util.Optional; import java.util.Properties; import java.util.ServiceLoader; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -108,6 +110,8 @@ public class BootstrapHandler extends SynchronizedRequestHandler { public static final String SERVICE_WORKER_HEADER = "Service-Worker"; + private static final String FETCH_DEST_HEADER = "Sec-Fetch-Dest"; + private static final CharSequence GWT_STAT_EVENTS_JS = "if (typeof window.__gwtStatsEvent != 'function') {" + "window.Vaadin.Flow.gwtStatsEvents = [];" + "window.__gwtStatsEvent = function(event) {" @@ -134,6 +138,32 @@ public class BootstrapHandler extends SynchronizedRequestHandler { private final PageBuilder pageBuilder; + private static final Set nonHtmlFetchDests; + + static { + // Full list at + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest + Set dests = new HashSet<>(); + dests.add("audio"); + dests.add("audioworklet"); + dests.add("font"); + dests.add("image"); + dests.add("manifest"); + dests.add("paintworklet"); + dests.add("script"); // NOSONAR + dests.add("serviceworker"); + dests.add("sharedworker"); + dests.add("style"); + dests.add("track"); + dests.add("video"); + dests.add("worker"); + dests.add("xslt"); + + // "empty" requests are used when service worker caches / so they need + // to be allowed + nonHtmlFetchDests = Collections.unmodifiableSet(dests); + } + /** * Creates an instance of the handler with default {@link PageBuilder}. */ @@ -511,15 +541,16 @@ protected boolean canHandleRequest(VaadinRequest request) { // an internal request return false; } - if (request.getHeader(SERVICE_WORKER_HEADER) != null) { - return false; - } if (isVaadinStaticFileRequest(request)) { // Do not allow routes inside /VAADIN/ return false; } + if (!isRequestForHtml(request)) { + return false; + } + return super.canHandleRequest(request); } @@ -572,6 +603,30 @@ public static boolean isVaadinStaticFileRequest(VaadinRequest request) { && request.getPathInfo().startsWith("/" + VAADIN_MAPPING); } + /** + * Checks if the request is potentially a request for an HTML page. + * + * @param request + * the request to check + * @return {@code true} if the request is potentially for HTML, + * {@code false} if it is certain that it is a request for a script, + * image or something else + */ + protected boolean isRequestForHtml(VaadinRequest request) { + if (request.getHeader(BootstrapHandler.SERVICE_WORKER_HEADER) != null) { + return false; + } + String fetchDest = request.getHeader(FETCH_DEST_HEADER); + if (fetchDest == null) { + // Old browsers do not send the header at all + return true; + } + if (nonHtmlFetchDests.contains(fetchDest)) { + return false; + } + return true; + } + @Override public boolean synchronizedHandleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java index 864c715e6a0..26f7843d0db 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java @@ -19,10 +19,7 @@ import java.io.IOException; import java.io.Serializable; import java.io.UncheckedIOException; -import java.util.Collections; -import java.util.HashSet; import java.util.Optional; -import java.util.Set; import org.jsoup.Jsoup; import org.jsoup.nodes.DataNode; @@ -69,30 +66,6 @@ public class IndexHtmlRequestHandler extends JavaScriptBootstrapHandler { private static final String SCRIPT = "script"; private static final String SCRIPT_INITIAL = "initial"; - private static final Set nonHtmlFetchDests; - static { - // Full list at - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest - Set dests = new HashSet<>(); - dests.add("audio"); - dests.add("audioworklet"); - dests.add("font"); - dests.add("image"); - dests.add("manifest"); - dests.add("paintworklet"); - dests.add("script"); // NOSONAR - dests.add("serviceworker"); - dests.add("sharedworker"); - dests.add("style"); - dests.add("track"); - dests.add("video"); - dests.add("worker"); - dests.add("xslt"); - - // "empty" requests are used when service worker caches / so they need - // to be allowed - nonHtmlFetchDests = Collections.unmodifiableSet(dests); - } @Override public boolean synchronizedHandleRequest(VaadinSession session, @@ -265,30 +238,6 @@ protected boolean canHandleRequest(VaadinRequest request) { .isValidUrl(request); } - /** - * Checks if the request is potentially a request for an HTML page. - * - * @param request - * the request to check - * @return {@code true} if the request is potentially for HTML, - * {@code false} if it is certain that it is a request for a script, - * image or something else - */ - protected boolean isRequestForHtml(VaadinRequest request) { - if (request.getHeader(SERVICE_WORKER_HEADER) != null) { - return false; - } - String fetchDest = request.getHeader("Sec-Fetch-Dest"); - if (fetchDest == null) { - // Old browsers do not send the header at all - return true; - } - if (nonHtmlFetchDests.contains(fetchDest)) { - return false; - } - return true; - } - @Override protected void initializeUIWithRouter(BootstrapContext context, UI ui) { if (context.getService().getBootstrapInitialPredicate() diff --git a/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java index ce49f142bf0..aa19247f086 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java @@ -1451,13 +1451,27 @@ private String contextRootRelativePath(VaadinRequest request) { } private VaadinServletRequest createVaadinRequest() { - HttpServletRequest request = createRequest(); + return createVaadinRequest(null); + } + + private VaadinServletRequest createVaadinRequest(String pathInfo) { + HttpServletRequest request = createRequest(pathInfo); return new VaadinServletRequest(request, service); } private HttpServletRequest createRequest() { + return createRequest(null); + } + + private HttpServletRequest createRequest(String pathInfo) { HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.doAnswer(invocation -> "").when(request).getServletPath(); + if (pathInfo != null) { + Mockito.doAnswer(invocation -> pathInfo).when(request) + .getPathInfo(); + Mockito.doAnswer(invocation -> new StringBuffer(pathInfo)) + .when(request).getRequestURL(); + } return request; } @@ -1591,6 +1605,56 @@ public void notServiceWorkerRequest_canHandleRequest() { Assert.assertTrue(bootstrapHandler.canHandleRequest(request)); } + @Test + public void canHandleRequest_allow_oldBrowser() { + BootstrapHandler bootstrapHandler = new BootstrapHandler(); + Assert.assertTrue(bootstrapHandler.canHandleRequest( + createRequestWithDestination("/", null, null))); + } + + @Test + public void canHandleRequest_handle_indexHtmlRequest() { + BootstrapHandler bootstrapHandler = new BootstrapHandler(); + Assert.assertTrue(bootstrapHandler.canHandleRequest( + createRequestWithDestination("/", "document", "navigate"))); + } + + @Test + public void canHandleRequest_doNotHandle_scriptRequest() { + BootstrapHandler bootstrapHandler = new BootstrapHandler(); + Assert.assertFalse(bootstrapHandler.canHandleRequest( + createRequestWithDestination("/", "script", "no-cors"))); + } + + @Test + public void canHandleRequest_doNotHandle_imageRequest() { + BootstrapHandler bootstrapHandler = new BootstrapHandler(); + Assert.assertFalse(bootstrapHandler.canHandleRequest( + createRequestWithDestination("/", "image", "no-cors"))); + } + + @Test + public void canHandleRequest_handle_serviceWorkerDocumentRequest() { + BootstrapHandler bootstrapHandler = new BootstrapHandler(); + Assert.assertTrue(bootstrapHandler.canHandleRequest( + createRequestWithDestination("/", "empty", "same-origin"))); + } + + private VaadinServletRequest createRequestWithDestination(String pathInfo, + String fetchDest, String fetchMode) { + VaadinServletRequest req = createVaadinRequest(pathInfo); + Mockito.when(req.getHeader(Mockito.anyString())).thenAnswer(arg -> { + if ("Sec-Fetch-Dest".equals(arg.getArgument(0))) { + return fetchDest; + } else if ("Sec-Fetch-Mode".equals(arg.getArgument(0))) { + return fetchMode; + } + return null; + }); + + return req; + } + @Test public void synchronizedHandleRequest_badLocation_noUiCreated() throws IOException {