All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.graylog2.shared.initializers.JerseyService Maven / Gradle / Ivy

There is a newer version: 6.0.2
Show newest version
/**
 * 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 .
 */
package org.graylog2.shared.initializers;

import com.codahale.metrics.InstrumentedExecutorService;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.glassfish.grizzly.http.CompressionConfig;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.NetworkListener;
import org.glassfish.grizzly.ssl.SSLContextConfigurator;
import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.model.Resource;
import org.graylog2.Configuration;
import org.graylog2.audit.PluginAuditEventTypes;
import org.graylog2.audit.jersey.AuditEventModelProcessor;
import org.graylog2.jersey.PrefixAddingModelProcessor;
import org.graylog2.plugin.rest.PluginRestResource;
import org.graylog2.rest.filter.WebAppNotFoundResponseFilter;
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;
import org.graylog2.shared.rest.exceptionmappers.AnyExceptionClassMapper;
import org.graylog2.shared.rest.exceptionmappers.BadRequestExceptionMapper;
import org.graylog2.shared.rest.exceptionmappers.JacksonPropertyExceptionMapper;
import org.graylog2.shared.rest.exceptionmappers.JsonProcessingExceptionMapper;
import org.graylog2.shared.rest.exceptionmappers.WebApplicationExceptionMapper;
import org.graylog2.shared.security.tls.KeyStoreUtils;
import org.graylog2.shared.security.tls.PemKeyStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.DynamicFeature;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.ExceptionMapper;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.stream.Collectors;

import static com.codahale.metrics.MetricRegistry.name;
import static com.google.common.base.MoreObjects.firstNonNull;

public class JerseyService extends AbstractIdleService {
    public static final String PLUGIN_PREFIX = "/plugins";
    private static final Logger LOG = LoggerFactory.getLogger(JerseyService.class);
    private static final String RESOURCE_PACKAGE_WEB = "org.graylog2.web.resources";

    private final Configuration configuration;
    private final Map>> pluginRestResources;
    private final String[] restControllerPackages;

    private final Set> dynamicFeatures;
    private final Set> containerResponseFilters;
    private final Set> exceptionMappers;
    private final Set additionalComponents;
    private final Set pluginAuditEventTypes;
    private final ObjectMapper objectMapper;
    private final MetricRegistry metricRegistry;

    private HttpServer apiHttpServer = null;
    private HttpServer webHttpServer = null;

    @Inject
    public JerseyService(final Configuration configuration,
                         Set> dynamicFeatures,
                         Set> containerResponseFilters,
                         Set> exceptionMappers,
                         @Named("additionalJerseyComponents") final Set additionalComponents,
                         final Map>> pluginRestResources,
                         @Named("RestControllerPackages") final String[] restControllerPackages,
                         Set pluginAuditEventTypes,
                         ObjectMapper objectMapper,
                         MetricRegistry metricRegistry) {
        this.configuration = configuration;
        this.dynamicFeatures = dynamicFeatures;
        this.containerResponseFilters = containerResponseFilters;
        this.exceptionMappers = exceptionMappers;
        this.additionalComponents = additionalComponents;
        this.pluginRestResources = pluginRestResources;
        this.restControllerPackages = restControllerPackages;
        this.pluginAuditEventTypes = pluginAuditEventTypes;
        this.objectMapper = objectMapper;
        this.metricRegistry = metricRegistry;
    }

    @Override
    protected void startUp() throws Exception {
        startUpApi();
        if (configuration.isWebEnable() && !configuration.isRestAndWebOnSamePort()) {
            startUpWeb();
        }
    }

    private void startUpWeb() throws Exception {
        final String[] resources = new String[]{RESOURCE_PACKAGE_WEB};

        final SSLEngineConfigurator sslEngineConfigurator = configuration.isWebEnableTls() ?
                buildSslEngineConfigurator(
                        configuration.getWebTlsCertFile(),
                        configuration.getWebTlsKeyFile(),
                        configuration.getWebTlsKeyPassword()) : null;

        final URI webListenUri = configuration.getWebListenUri();
        final URI listenUri = new URI(
                webListenUri.getScheme(),
                webListenUri.getUserInfo(),
                webListenUri.getHost(),
                webListenUri.getPort(),
                null,
                null,
                null
        );

        webHttpServer = setUp("web",
                listenUri,
                sslEngineConfigurator,
                configuration.getWebThreadPoolSize(),
                configuration.getWebSelectorRunnersCount(),
                configuration.getWebMaxInitialLineLength(),
                configuration.getWebMaxHeaderSize(),
                configuration.isWebEnableGzip(),
                configuration.isWebEnableCors(),
                Collections.emptySet(),
                resources);

        webHttpServer.start();

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

    @Override
    protected void shutDown() throws Exception {
        shutdownHttpServer(apiHttpServer, configuration.getRestListenUri());
        shutdownHttpServer(webHttpServer, configuration.getWebListenUri());
    }

    private void shutdownHttpServer(HttpServer httpServer, URI listenUri) {
        if (httpServer != null && httpServer.isStarted()) {
            LOG.info("Shutting down HTTP listener at <{}>", listenUri);
            httpServer.shutdownNow();
        }
    }

    private void startUpApi() throws Exception {
        final boolean startWebInterface = configuration.isWebEnable() && configuration.isRestAndWebOnSamePort();
        final List resourcePackages = new ArrayList<>(Arrays.asList(restControllerPackages));

        if (startWebInterface) {
            resourcePackages.add(RESOURCE_PACKAGE_WEB);
        }

        final Set pluginResources = prefixPluginResources(PLUGIN_PREFIX, pluginRestResources);

        final SSLEngineConfigurator sslEngineConfigurator = configuration.isRestEnableTls() ?
                buildSslEngineConfigurator(
                        configuration.getRestTlsCertFile(),
                        configuration.getRestTlsKeyFile(),
                        configuration.getRestTlsKeyPassword()) : null;

        final URI restListenUri = configuration.getRestListenUri();
        final URI listenUri = new URI(
                restListenUri.getScheme(),
                restListenUri.getUserInfo(),
                restListenUri.getHost(),
                restListenUri.getPort(),
                null,
                null,
                null
        );

        apiHttpServer = setUp("rest",
                listenUri,
                sslEngineConfigurator,
                configuration.getRestThreadPoolSize(),
                configuration.getRestSelectorRunnersCount(),
                configuration.getRestMaxInitialLineLength(),
                configuration.getRestMaxHeaderSize(),
                configuration.isRestEnableGzip(),
                configuration.isRestEnableCors(),
                pluginResources,
                resourcePackages.toArray(new String[0]));

        apiHttpServer.start();

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

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

    private Set prefixPluginResources(String pluginPrefix, Map>> pluginResourceMap) {
        return pluginResourceMap.entrySet().stream()
                .map(entry -> prefixResources(pluginPrefix + "/" + entry.getKey(), entry.getValue()))
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());
    }

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

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

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


    private ResourceConfig buildResourceConfig(final boolean enableCors,
                                               final Set additionalResources,
                                               final String[] controllerPackages) {
        final Map packagePrefixes = new HashMap<>();
        for (String resourcePackage : controllerPackages) {
            packagePrefixes.put(resourcePackage, configuration.getRestListenUri().getPath());
        }
        packagePrefixes.put(RESOURCE_PACKAGE_WEB, configuration.getWebListenUri().getPath());
        packagePrefixes.put("", configuration.getRestListenUri().getPath());

        final ResourceConfig rc = new ResourceConfig()
                .property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true)
                .property(ServerProperties.WADL_FEATURE_DISABLE, true)
                .register(new PrefixAddingModelProcessor(packagePrefixes))
                .register(new AuditEventModelProcessor(pluginAuditEventTypes))
                .registerClasses(
                        JacksonJaxbJsonProvider.class,
                        JsonProcessingExceptionMapper.class,
                        JacksonPropertyExceptionMapper.class,
                        AnyExceptionClassMapper.class,
                        WebApplicationExceptionMapper.class,
                        BadRequestExceptionMapper.class,
                        RestAccessLogFilter.class,
                        NodeIdResponseFilter.class,
                        XHRFilter.class,
                        NotAuthorizedResponseFilter.class,
                        WebAppNotFoundResponseFilter.class)
                .register(new ContextResolver() {
                    @Override
                    public ObjectMapper getContext(Class type) {
                        return objectMapper;
                    }
                })
                .packages(true, controllerPackages)
                .registerResources(additionalResources);

        exceptionMappers.forEach(rc::registerClasses);
        dynamicFeatures.forEach(rc::registerClasses);
        containerResponseFilters.forEach(rc::registerClasses);
        additionalComponents.forEach(rc::registerClasses);

        if (enableCors) {
            LOG.info("Enabling CORS for HTTP endpoint");
            rc.registerClasses(CORSFilter.class);
        }

        if (LOG.isDebugEnabled()) {
            rc.registerClasses(PrintModelProcessor.class);
        }

        return rc;
    }

    private HttpServer setUp(String namePrefix,
                             URI listenUri,
                             SSLEngineConfigurator sslEngineConfigurator,
                             int threadPoolSize,
                             int selectorRunnersCount,
                             int maxInitialLineLength,
                             int maxHeaderSize,
                             boolean enableGzip,
                             boolean enableCors,
                             Set additionalResources,
                             String[] controllerPackages)
            throws GeneralSecurityException, IOException {
        final ResourceConfig resourceConfig = buildResourceConfig(
                enableCors,
                additionalResources,
                controllerPackages
        );

        final HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer(
                listenUri,
                resourceConfig,
                sslEngineConfigurator != null,
                sslEngineConfigurator,
                false);

        final NetworkListener listener = httpServer.getListener("grizzly");
        listener.setMaxHttpHeaderSize(maxInitialLineLength);
        listener.setMaxRequestHeaders(maxHeaderSize);

        final ExecutorService workerThreadPoolExecutor = instrumentedExecutor(
                namePrefix + "-worker-executor",
                namePrefix + "-worker-%d",
                threadPoolSize);
        listener.getTransport().setWorkerThreadPool(workerThreadPoolExecutor);

        // The Grizzly default value is equal to `Runtime.getRuntime().availableProcessors()` which doesn't make
        // sense for Graylog because we are not mainly a web server.
        // See "Selector runners count" at https://grizzly.java.net/bestpractices.html for details.
        listener.getTransport().setSelectorRunnersCount(selectorRunnersCount);


        if(enableGzip) {
            final CompressionConfig compressionConfig = listener.getCompressionConfig();
            compressionConfig.setCompressionMode(CompressionConfig.CompressionMode.ON);
            compressionConfig.setCompressionMinSize(512);
        }

        return httpServer;
    }

    private SSLEngineConfigurator buildSslEngineConfigurator(Path certFile, Path keyFile, String keyPassword)
            throws GeneralSecurityException, IOException {
        if (keyFile == null || !Files.isRegularFile(keyFile) || !Files.isReadable(keyFile)) {
            throw new InvalidKeyException("Unreadable or missing private key: " + keyFile);
        }

        if (certFile == null || !Files.isRegularFile(certFile) || !Files.isReadable(certFile)) {
            throw new CertificateException("Unreadable or missing X.509 certificate: " + certFile);
        }

        final SSLContextConfigurator sslContext = new SSLContextConfigurator();
        final char[] password = firstNonNull(keyPassword, "").toCharArray();
        final KeyStore keyStore = PemKeyStore.buildKeyStore(certFile, keyFile, password);
        sslContext.setKeyStorePass(password);
        sslContext.setKeyStoreBytes(KeyStoreUtils.getBytes(keyStore, password));

        if (!sslContext.validateConfiguration(true)) {
            throw new IllegalStateException("Couldn't initialize SSL context for HTTP server");
        }

        return new SSLEngineConfigurator(sslContext.createSSLContext(), false, false, false);
    }

    private ExecutorService instrumentedExecutor(final String executorName,
                                                 final String threadNameFormat,
                                                 int poolSize) {
        final ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setNameFormat(threadNameFormat)
                .setDaemon(true)
                .build();

        return new InstrumentedExecutorService(
                Executors.newFixedThreadPool(poolSize, threadFactory),
                metricRegistry,
                name(JerseyService.class, executorName));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy