Skip to content

Commit

Permalink
feat: Only send index.html when the browser requests a HTML page (#13242
Browse files Browse the repository at this point in the history
)

Same as #12571 but for V14 bootstrapping mode.

Fixes #10965 for V14 bootstrapping mode.
  • Loading branch information
anssit authored Mar 11, 2022
1 parent 29fe2b8 commit 428e7bc
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
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;
import java.util.Map;
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;
Expand Down Expand Up @@ -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) {"
Expand All @@ -134,6 +138,32 @@ public class BootstrapHandler extends SynchronizedRequestHandler {

private final PageBuilder pageBuilder;

private static final Set<String> nonHtmlFetchDests;

static {
// Full list at
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest
Set<String> 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}.
*/
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> nonHtmlFetchDests;
static {
// Full list at
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest
Set<String> 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,
Expand Down Expand Up @@ -261,30 +234,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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 428e7bc

Please sign in to comment.