Skip to content

Commit

Permalink
Enable running REST API and web interface on same port. (#2515)
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisoelkers authored and joschi committed Jul 25, 2016
1 parent 617fc29 commit c5381f6
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 22 deletions.
Expand Up @@ -30,7 +30,6 @@
import org.graylog2.auditlog.AuditLogger;
import org.graylog2.bindings.AlarmCallbackBindings;
import org.graylog2.bindings.ConfigurationModule;
import org.graylog2.decorators.DecoratorBindings;
import org.graylog2.bindings.InitializerBindings;
import org.graylog2.bindings.MessageFilterBindings;
import org.graylog2.bindings.MessageOutputBindings;
Expand All @@ -48,6 +47,7 @@
import org.graylog2.configuration.MongoDbConfiguration;
import org.graylog2.configuration.VersionCheckConfiguration;
import org.graylog2.dashboards.DashboardBindings;
import org.graylog2.decorators.DecoratorBindings;
import org.graylog2.indexer.retention.RetentionStrategyBindings;
import org.graylog2.indexer.rotation.RotationStrategyBindings;
import org.graylog2.messageprocessors.MessageProcessorModule;
Expand Down Expand Up @@ -120,7 +120,7 @@ protected List<Module> getCommandBindings() {
new AuditLogModule()
);

if (configuration.isWebEnable()) {
if (configuration.isWebEnable() && !configuration.isRestAndWebOnSamePort()) {
modules.add(new WebInterfaceModule());
}

Expand Down
Expand Up @@ -24,6 +24,7 @@
import com.github.joschi.jadconfig.validators.PositiveIntegerValidator;
import com.github.joschi.jadconfig.validators.StringNotBlankValidator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.SleepingWaitStrategy;
Expand All @@ -33,7 +34,10 @@
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;

Expand Down Expand Up @@ -354,6 +358,18 @@ public boolean isWebEnable() {
return webEnable;
}

public boolean isRestAndWebOnSamePort() {
final URI restListenUri = getRestListenUri();
final URI webListenUri = getWebListenUri();
try {
final InetAddress restAddress = InetAddress.getByName(restListenUri.getHost());
final InetAddress webAddress = InetAddress.getByName(webListenUri.getHost());
return restListenUri.getPort() == webListenUri.getPort() && restAddress.equals(webAddress);
} catch (UnknownHostException e) {
throw new RuntimeException("Unable to resolve hostnames of rest/web listen uris: ", e);
}
}

public boolean isWebEnableCors() {
return webEnableCors;
}
Expand Down Expand Up @@ -403,6 +419,7 @@ public String getWebPrefix() {
}

@ValidatorMethod
@SuppressWarnings("unused")
public void validateRestTlsConfig() throws ValidationException {
if(isRestEnableTls()) {
if(!isRegularFileAndReadable(getRestTlsKeyFile())) {
Expand All @@ -416,6 +433,7 @@ public void validateRestTlsConfig() throws ValidationException {
}

@ValidatorMethod
@SuppressWarnings("unused")
public void validateWebTlsConfig() throws ValidationException {
if(isWebEnableTls()) {
if(!isRegularFileAndReadable(getWebTlsKeyFile())) {
Expand All @@ -428,6 +446,32 @@ public void validateWebTlsConfig() throws ValidationException {
}
}

@ValidatorMethod
@SuppressWarnings("unused")
public void validateRestAndWebListenConfigConflict() throws ValidationException {
if (isRestAndWebOnSamePort()) {
if (getRestListenUri().getPath().equals(getWebListenUri().getPath())) {
throw new ValidationException("If REST and Web interface are served on the same host/port, the path must be different!");
}
}
}

@ValidatorMethod
@SuppressWarnings("unused")
public void validateWebHasPathPrefixIfOnSamePort() throws ValidationException {
if (isRestAndWebOnSamePort() && (Strings.isNullOrEmpty(getWebPrefix()) || getWebPrefix().equals("/"))) {
throw new ValidationException("If REST and Web Interface are served on the same host/port, the web interface must have a path prefix!");
}
}

@ValidatorMethod
@SuppressWarnings("unused")
public void validateWebAndRestHaveSameProtocolIfOnSamePort() throws ValidationException {
if (isRestAndWebOnSamePort() && !getWebListenUri().getScheme().equals(getRestListenUri().getScheme())) {
throw new ValidationException("If REST and Web interface are served on the same host/port, the protocols must be identical!");
}
}

private boolean isRegularFileAndReadable(Path path) {
return path != null && Files.isRegularFile(path) && Files.isReadable(path);
}
Expand Down
Expand Up @@ -19,7 +19,7 @@
import com.codahale.metrics.annotation.Timed;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.graylog2.Configuration;
import org.graylog2.plugin.Version;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.plugin.cluster.ClusterId;
Expand All @@ -32,6 +32,7 @@
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import static java.util.Objects.requireNonNull;

Expand All @@ -40,11 +41,15 @@
public class HelloWorldResource extends RestResource {
private final NodeId nodeId;
private final ClusterConfigService clusterConfigService;
private final Configuration configuration;

@Inject
public HelloWorldResource(NodeId nodeId, ClusterConfigService clusterConfigService) {
public HelloWorldResource(NodeId nodeId,
ClusterConfigService clusterConfigService,
Configuration configuration) {
this.nodeId = requireNonNull(nodeId);
this.clusterConfigService = requireNonNull(clusterConfigService);
this.configuration = configuration;
}

@GET
Expand All @@ -60,4 +65,21 @@ public HelloWorldResponse helloWorld() {
"Manage your logs in the dark and have lasers going and make it look like you're from space!"
);
}

@GET
@Timed
@ApiOperation(value = "Redirecting to web console if it runs on same port.")
@Produces({MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML})
public Response redirectToWebConsole() {
if (configuration.isRestAndWebOnSamePort()) {
return Response
.temporaryRedirect(configuration.getWebListenUri())
.build();
}

return Response
.ok(helloWorld())
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Expand Up @@ -35,6 +35,7 @@
import org.glassfish.jersey.server.model.Resource;
import org.graylog2.shared.rest.CORSFilter;
import org.graylog2.shared.rest.NodeIdResponseFilter;
import org.graylog2.shared.rest.NotAuthorizedResponseFilter;
import org.graylog2.shared.rest.PrintModelProcessor;
import org.graylog2.shared.rest.RestAccessLogFilter;
import org.graylog2.shared.rest.XHRFilter;
Expand Down Expand Up @@ -119,7 +120,8 @@ public ObjectMapper getContext(Class<?> type) {
.registerResources(additionalResources)
.register(RestAccessLogFilter.class)
.register(NodeIdResponseFilter.class)
.register(XHRFilter.class);
.register(XHRFilter.class)
.register(NotAuthorizedResponseFilter.class);

exceptionMappers.forEach(rc::registerClasses);
dynamicFeatures.forEach(rc::registerClasses);
Expand Down
Expand Up @@ -18,9 +18,14 @@

import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.glassfish.jersey.server.model.Resource;
import org.graylog2.plugin.BaseConfiguration;
import org.graylog2.plugin.rest.PluginRestResource;
import org.graylog2.web.resources.AppConfigResource;
import org.graylog2.web.resources.WebInterfaceAssetsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -31,9 +36,12 @@
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.DynamicFeature;
import javax.ws.rs.ext.ExceptionMapper;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

@Singleton
public class RestApiService extends AbstractJerseyService {
Expand All @@ -43,6 +51,8 @@ public class RestApiService extends AbstractJerseyService {
private final BaseConfiguration configuration;
private final Map<String, Set<PluginRestResource>> pluginRestResources;
private final String[] restControllerPackages;
private final WebInterfaceAssetsResource webInterfaceAssetsResource;
private final AppConfigResource appConfigResource;

@Inject
private RestApiService(final BaseConfiguration configuration,
Expand All @@ -53,15 +63,28 @@ private RestApiService(final BaseConfiguration configuration,
@Named("additionalJerseyComponents") final Set<Class> additionalComponents,
final Map<String, Set<PluginRestResource>> pluginRestResources,
@Named("RestControllerPackages") final String[] restControllerPackages,
final ObjectMapper objectMapper) {
final ObjectMapper objectMapper,
WebInterfaceAssetsResource webInterfaceAssetsResource,
AppConfigResource appConfigResource) {
super(dynamicFeatures, containerResponseFilters, exceptionMappers, additionalComponents, objectMapper, metricRegistry);
this.configuration = configuration;
this.pluginRestResources = pluginRestResources;
this.restControllerPackages = restControllerPackages;
this.webInterfaceAssetsResource = webInterfaceAssetsResource;
this.appConfigResource = appConfigResource;
}

@Override
protected void startUp() throws Exception {
ImmutableSet.Builder<Resource> additionalResourcesBuilder = ImmutableSet
.<Resource>builder()
.addAll(prefixPluginResources(PLUGIN_PREFIX, pluginRestResources));

if (configuration.isWebEnable() && configuration.isRestAndWebOnSamePort()) {
additionalResourcesBuilder = additionalResourcesBuilder
.addAll(prefixResources(configuration.getWebPrefix(), ImmutableSet.of(webInterfaceAssetsResource, appConfigResource)));
}

httpServer = setUp("rest",
configuration.getRestListenUri(),
configuration.isRestEnableTls(),
Expand All @@ -73,30 +96,41 @@ protected void startUp() throws Exception {
configuration.getRestMaxHeaderSize(),
configuration.isRestEnableGzip(),
configuration.isRestEnableCors(),
prefixPluginResources(PLUGIN_PREFIX, pluginRestResources),
additionalResourcesBuilder.build(),
restControllerPackages);

httpServer.start();

LOG.info("Started REST API at <{}>", configuration.getRestListenUri());

if (configuration.isWebEnable() && configuration.isRestAndWebOnSamePort()) {
LOG.info("Started Web Interface at <{}>", configuration.getWebListenUri());
}
}

private Set<Resource> prefixPluginResources(String pluginPrefix, Map<String, Set<PluginRestResource>> pluginResourceMap) {
final Set<Resource> result = new HashSet<>();
for (Map.Entry<String, Set<PluginRestResource>> entry : pluginResourceMap.entrySet()) {
for (PluginRestResource pluginRestResource : entry.getValue()) {
StringBuilder resourcePath = new StringBuilder(pluginPrefix).append("/").append(entry.getKey());
final Path pathAnnotation = Resource.getPath(pluginRestResource.getClass());
final String path = (pathAnnotation.value() == null ? "" : pathAnnotation.value());
if (!path.startsWith("/"))
resourcePath.append("/");

final Resource.Builder resourceBuilder = Resource.builder(pluginRestResource.getClass()).path(resourcePath.append(path).toString());
final Resource resource = resourceBuilder.build();
result.add(resource);
}
}
return result;
return pluginResourceMap.entrySet().stream()
.map(entry -> prefixResources(pluginPrefix + "/" + entry.getKey(), entry.getValue()))
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}

private <T> Set<Resource> prefixResources(String prefix, Set<T> resources) {
final String pathPrefix = prefix.endsWith("/") ? prefix.substring(0, prefix.length()-1) : prefix;

return resources
.stream()
.map(resource -> {
final Path pathAnnotation = Resource.getPath(resource.getClass());
final String resourcePathSuffix = Strings.nullToEmpty(pathAnnotation.value());
final String resourcePath = resourcePathSuffix.startsWith("/") ? pathPrefix + resourcePathSuffix : pathPrefix + "/" + resourcePathSuffix;

return Resource
.builder(resource.getClass())
.path(resourcePath)
.build();
})
.collect(Collectors.toSet());
}

@Override
Expand Down
@@ -0,0 +1,38 @@
/**
* This file is part of Graylog.
*
* Graylog is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog2.shared.rest;

import com.google.common.net.HttpHeaders;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Response;
import java.io.IOException;

public class NotAuthorizedResponseFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
if (responseContext.getStatusInfo().equals(Response.Status.UNAUTHORIZED)) {
final String requestedWith = requestContext.getHeaderString(HttpHeaders.X_REQUESTED_WITH);
if ("XMLHttpRequest".equalsIgnoreCase(requestedWith)) {
responseContext.getHeaders().remove(HttpHeaders.WWW_AUTHENTICATE);

}
}
}
}

0 comments on commit c5381f6

Please sign in to comment.