Skip to content

Commit

Permalink
Handle path prefix when serving web interface assets. (#21104)
Browse files Browse the repository at this point in the history
* Handle path prefix when serving web interface assets.

* Adding tests for frontend asset consistency.

* Adding changelog snippet.
  • Loading branch information
dennisoelkers committed Dec 19, 2024
1 parent 5d333f6 commit ab65275
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 23 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-21015.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "f"
message = "Handle path prefix when serving web interface assets."

issues = ["21015"]
pulls = ["21104"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.web.resources;

import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import org.apache.http.HttpStatus;
import org.graylog.testing.completebackend.apis.GraylogApis;

import static io.restassured.RestAssured.given;

public abstract class WebInterfaceAssetsResourceBase {
private final GraylogApis apis;

protected WebInterfaceAssetsResourceBase(GraylogApis apis) {
this.apis = apis;
}

private RequestSpecification backend() {
return given()
.baseUri(apis.backend().uri())
.port(apis.backend().apiPort());
}

protected void testFrontend(String prefix) {
final var scriptSrcs = backend()
.get(prefix)
.then()
.assertThat()
.statusCode(HttpStatus.SC_OK)
.contentType(ContentType.HTML)
.extract()
.htmlPath()
.<String>getList("html.body.script*.@src");

scriptSrcs.forEach(src -> {
backend()
.get(src)
.then()
.assertThat()
.statusCode(HttpStatus.SC_OK)
.contentType(ContentType.JSON);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.web.resources;

import org.graylog.testing.completebackend.MavenProjectDirProviderWithFrontend;
import org.graylog.testing.completebackend.apis.GraylogApis;
import org.graylog.testing.containermatrix.SearchServer;
import org.graylog.testing.containermatrix.annotations.ContainerMatrixTest;
import org.graylog.testing.containermatrix.annotations.ContainerMatrixTestsConfiguration;

@ContainerMatrixTestsConfiguration(mavenProjectDirProvider = MavenProjectDirProviderWithFrontend.class,
searchVersions = {SearchServer.DATANODE_DEV})
public class WebInterfaceAssetsResourceIT extends WebInterfaceAssetsResourceBase {
public WebInterfaceAssetsResourceIT(GraylogApis graylogApis) {
super(graylogApis);
}

@ContainerMatrixTest
void testIndexHtml() {
testFrontend("/");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.web.resources;

import org.graylog.testing.completebackend.MavenProjectDirProviderWithFrontend;
import org.graylog.testing.completebackend.apis.GraylogApis;
import org.graylog.testing.containermatrix.SearchServer;
import org.graylog.testing.containermatrix.annotations.ContainerMatrixTest;
import org.graylog.testing.containermatrix.annotations.ContainerMatrixTestsConfiguration;

@ContainerMatrixTestsConfiguration(mavenProjectDirProvider = MavenProjectDirProviderWithFrontend.class,
searchVersions = {SearchServer.DATANODE_DEV},
additionalConfigurationParameters = {
@ContainerMatrixTestsConfiguration.ConfigurationParameter(key = "GRAYLOG_HTTP_PUBLISH_URI", value = "http://localhost:9000/graylog")
})
public class WebInterfaceAssetsResourceWithPrefixIT extends WebInterfaceAssetsResourceBase {
public WebInterfaceAssetsResourceWithPrefixIT(GraylogApis graylogApis) {
super(graylogApis);
}

@ContainerMatrixTest
void testIndexHtml() {
testFrontend("/graylog/");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,8 @@
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import org.glassfish.jersey.server.ContainerRequest;
import org.graylog2.plugin.Plugin;
import org.graylog2.shared.rest.resources.csp.CSP;
import org.graylog2.shared.rest.resources.csp.CSPDynamicFeature;
import org.graylog2.web.IndexHtmlGenerator;
import org.graylog2.web.PluginAssets;

import javax.activation.MimetypesFileTypeMap;
import javax.annotation.Nonnull;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
Expand All @@ -46,7 +35,17 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import org.glassfish.jersey.server.ContainerRequest;
import org.graylog2.configuration.HttpConfiguration;
import org.graylog2.plugin.Plugin;
import org.graylog2.rest.RestTools;
import org.graylog2.shared.rest.resources.csp.CSP;
import org.graylog2.shared.rest.resources.csp.CSPDynamicFeature;
import org.graylog2.web.IndexHtmlGenerator;
import org.graylog2.web.PluginAssets;

import javax.activation.MimetypesFileTypeMap;
import javax.annotation.Nonnull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
Expand All @@ -73,15 +72,20 @@
@CSP(group = CSP.DEFAULT)
public class WebInterfaceAssetsResource {
private final MimetypesFileTypeMap mimeTypes;
private final HttpConfiguration httpConfiguration;
private final IndexHtmlGenerator indexHtmlGenerator;
private final Set<Plugin> plugins;
private final LoadingCache<URI, FileSystem> fileSystemCache;

@Inject
public WebInterfaceAssetsResource(IndexHtmlGenerator indexHtmlGenerator, Set<Plugin> plugins, MimetypesFileTypeMap mimeTypes) {
public WebInterfaceAssetsResource(IndexHtmlGenerator indexHtmlGenerator,
Set<Plugin> plugins,
MimetypesFileTypeMap mimeTypes,
HttpConfiguration httpConfiguration) {
this.indexHtmlGenerator = indexHtmlGenerator;
this.plugins = plugins;
this.mimeTypes = requireNonNull(mimeTypes);
this.httpConfiguration = httpConfiguration;
this.fileSystemCache = CacheBuilder.newBuilder()
.maximumSize(1024)
.build(new CacheLoader<>() {
Expand All @@ -103,16 +107,18 @@ public FileSystem load(@Nonnull URI key) throws Exception {
@Path("assets/plugin/{plugin}/{filename}")
@GET
public Response get(@Context Request request,
@Context HttpHeaders headers,
@PathParam("plugin") String pluginName,
@PathParam("filename") String filename) {
final Plugin plugin = getPluginForName(pluginName)
.orElseThrow(() -> new NotFoundException("Couldn't find plugin " + pluginName));
final var filenameWithoutSuffix = trimBasePath(filename, headers);

try {
final URL resourceUrl = getResourceUri(true, filename, plugin.metadata().getClass());
return getResponse(request, filename, resourceUrl, true);
final URL resourceUrl = getResourceUri(true, filenameWithoutSuffix, plugin.metadata().getClass());
return getResponse(request, filenameWithoutSuffix, resourceUrl, true);
} catch (URISyntaxException | IOException e) {
throw new NotFoundException("Couldn't find " + filename + " in plugin " + pluginName, e);
throw new NotFoundException("Couldn't find " + filenameWithoutSuffix + " in plugin " + pluginName, e);
}
}

Expand All @@ -125,17 +131,33 @@ private Optional<Plugin> getPluginForName(String pluginName) {
public Response get(@Context ContainerRequest request,
@Context HttpHeaders headers,
@PathParam("filename") String filename) {
final var filenameWithoutSuffix = trimBasePath(filename, headers);
try {
final URL resourceUrl = getResourceUri(false, filename, this.getClass());
return getResponse(request, filename, resourceUrl, false);
final URL resourceUrl = getResourceUri(false, filenameWithoutSuffix, this.getClass());
return getResponse(request, filenameWithoutSuffix, resourceUrl, false);
} catch (IOException | URISyntaxException e) {
return generateIndexHtml(headers, (String) request.getProperty(CSPDynamicFeature.CSP_NONCE_PROPERTY));
}

}

private String trimBasePath(String filename, HttpHeaders headers) {
final String baseUriPath = removeTrailingSlash(RestTools.buildRelativeExternalUri(headers.getRequestHeaders(), httpConfiguration.getHttpExternalUri()).getPath());
return filename.startsWith(baseUriPath) ? filename.substring(baseUriPath.length()) : filename;
}

private String removeTrailingSlash(String basePath) {
if (basePath == null || !basePath.endsWith("/")) {
return basePath;
}

return basePath.substring(0, basePath.length() - 1);
}

@GET
@Path("{filename:.*}")
public Response getIndex(@Context ContainerRequest request, @Context HttpHeaders headers) {
public Response getIndex(@Context ContainerRequest request,
@Context HttpHeaders headers) {
final URI originalLocation = request.getRequestUri();
return get(request, headers, originalLocation.getPath());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.testing.completebackend;

public class MavenProjectDirProviderWithFrontend extends DefaultMavenProjectDirProvider {
@Override
public boolean includeFrontend() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.utility.MountableFile;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.StreamSupport;

import static java.time.temporal.ChronoUnit.SECONDS;
Expand Down Expand Up @@ -165,7 +166,7 @@ private static GenericContainer<?> createRunningContainer(NodeContainerConfig co
config.mavenProjectDirProvider.getFilesToAddToBinDir().forEach(filename -> {
final Path originalPath = fileCopyBaseDir.resolve(filename);
final String containerPath = GRAYLOG_HOME + "/bin/" + originalPath.getFileName();
container.addFileSystemBind(originalPath.toString(), containerPath.toString(), BindMode.READ_ONLY);
container.addFileSystemBind(originalPath.toString(), containerPath, BindMode.READ_ONLY);
});

addEnabledFeatureFlagsToContainerEnv(config, container);
Expand Down Expand Up @@ -193,7 +194,11 @@ private static WaitAllStrategy getWaitStrategy(Map<String, String> env) {
if(indexerIsPredefined(env)) { // we have defined an indexer, no preflight will occur, let's wait for the full boot with index ranges
// To be able to search for data we need the index ranges to be computed. Since this is an async
// background job, we need to wait until they have been created.
waitAllStrategy.withStrategy(waitForIndexRangesStrategy());
final var baseUrl = Optional.ofNullable(env.get("GRAYLOG_HTTP_PUBLISH_URI"))
.map(URI::create)
.map(URI::getPath)
.orElse("");
waitAllStrategy.withStrategy(waitForIndexRangesStrategy(baseUrl));
}

return waitAllStrategy;
Expand All @@ -203,10 +208,10 @@ private static boolean indexerIsPredefined(Map<String, String> env) {
return !env.getOrDefault(ENV_GRAYLOG_ELASTICSEARCH_HOSTS, "").isBlank();
}

private static HttpWaitStrategy waitForIndexRangesStrategy() {
private static HttpWaitStrategy waitForIndexRangesStrategy(String urlPrefix) {
return new HttpWaitStrategy()
.forPort(API_PORT)
.forPath("/api/system/indices/ranges")
.forPath(urlPrefix + "/api/system/indices/ranges")
.withMethod("GET")
.withBasicCredentials("admin", "admin")
.forResponsePredicate(body -> {
Expand Down

0 comments on commit ab65275

Please sign in to comment.