Skip to content

Commit

Permalink
NIFI-14115 Set Standard HTTP Headers for Framework Responses
Browse files Browse the repository at this point in the history
This closes #9598

- Added HeaderWriterHandler implementing Jetty Handler methods for setting standard HTTP response headers
- Removed HeaderWriterFilter approach for writing standard HTTP headers
- Refactored Jetty Server instantiation to StandardServerProvider for streamlined configuration and testing
- Simplified Server start test method to avoid timing issues
- Removed unnecessary Timeout annotation

Signed-off-by: Joseph Witt <[email protected]>
  • Loading branch information
exceptionfactory authored and joewitt committed Dec 27, 2024
1 parent 3897959 commit 5f72313
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
Expand All @@ -36,7 +32,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -103,14 +98,11 @@
import org.apache.nifi.web.ContentAccess;
import org.apache.nifi.web.NiFiWebConfigurationContext;
import org.apache.nifi.web.UiExtensionType;
import org.apache.nifi.web.server.connector.FrameworkServerConnectorFactory;
import org.apache.nifi.web.server.filter.FilterParameter;
import org.apache.nifi.web.server.filter.LogoutCompleteRedirectFilter;
import org.apache.nifi.web.server.filter.RequestFilterProvider;
import org.apache.nifi.web.server.filter.RestApiRequestFilterProvider;
import org.apache.nifi.web.server.filter.StandardRequestFilterProvider;
import org.apache.nifi.web.server.log.RequestLogProvider;
import org.apache.nifi.web.server.log.StandardRequestLogProvider;
import org.eclipse.jetty.deploy.App;
import org.eclipse.jetty.deploy.AppProvider;
import org.eclipse.jetty.deploy.DeploymentManager;
Expand All @@ -121,7 +113,6 @@
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
Expand All @@ -130,7 +121,6 @@
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.ee10.webapp.WebAppClassLoader;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.slf4j.Logger;
Expand Down Expand Up @@ -222,42 +212,29 @@ public JettyServer() {

public void init() {
clearWorkingDirectory();
final QueuedThreadPool threadPool = new QueuedThreadPool(props.getWebThreads());
threadPool.setName("NiFi Web Server");
this.server = new Server(threadPool);
final FrameworkSslContextProvider sslContextProvider = new FrameworkSslContextProvider(props);
this.sslContext = sslContextProvider.loadSslContext().orElse(null);

configureConnectors(server);

final ContextHandlerCollection handlerCollection = new ContextHandlerCollection();
final Handler standardHandler = getStandardHandler(handlerCollection);
server.setHandler(standardHandler);

final RewriteHandler defaultRewriteHandler = new RewriteHandler();
final RedirectPatternRule redirectDefault = new RedirectPatternRule("/*", "/nifi");
defaultRewriteHandler.addRule(redirectDefault);
server.setDefaultHandler(defaultRewriteHandler);

deploymentManager.setContexts(handlerCollection);
server.addBean(deploymentManager);

final String requestLogFormat = props.getProperty(NiFiProperties.WEB_REQUEST_LOG_FORMAT);
final RequestLogProvider requestLogProvider = new StandardRequestLogProvider(requestLogFormat);
final RequestLog requestLog = requestLogProvider.getRequestLog();
server.setRequestLog(requestLog);
}

private Handler getStandardHandler(final ContextHandlerCollection handlerCollection) {
// Only restrict the host header if running in HTTPS mode
if (props.isHTTPSConfigured()) {
final HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
handlerCollection.addHandler(hostHeaderHandler);
}
try {
final FrameworkSslContextProvider sslContextProvider = new FrameworkSslContextProvider(props);
sslContext = sslContextProvider.loadSslContext().orElse(null);

final Handler warHandlers = loadInitialWars(bundles);
handlerCollection.addHandler(warHandlers);
return handlerCollection;
final ServerProvider serverProvider = new StandardServerProvider(sslContext);
server = serverProvider.getServer(props);

final Handler serverHandler = server.getHandler();
if (serverHandler instanceof Handler.Collection serverHandlerCollection) {
final ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
final Handler warHandlers = loadInitialWars(bundles);
contextHandlerCollection.addHandler(warHandlers);
deploymentManager.setContexts(contextHandlerCollection);
server.addBean(deploymentManager);

serverHandlerCollection.addHandler(contextHandlerCollection);
} else {
throw new IllegalStateException("Server Handler not Handler.Collection: Server Provider configuration failed");
}
} catch (final Throwable e) {
startUpFailure(e);
}
}

private void clearWorkingDirectory() {
Expand Down Expand Up @@ -771,50 +748,6 @@ private File getWebApiDocsDir() {
return webApiDocsDir;
}

private void configureConnectors(final Server server) {
try {
final FrameworkServerConnectorFactory serverConnectorFactory = new FrameworkServerConnectorFactory(server, props);
if (props.isHTTPSConfigured()) {
serverConnectorFactory.setSslContext(sslContext);
}

final Map<String, String> interfaces = props.isHTTPSConfigured() ? props.getHttpsNetworkInterfaces() : props.getHttpNetworkInterfaces();
final Set<String> interfaceNames = interfaces.values().stream().filter(StringUtils::isNotBlank).collect(Collectors.toSet());
// Add Server Connectors based on configured Network Interface Names
if (interfaceNames.isEmpty()) {
final ServerConnector serverConnector = serverConnectorFactory.getServerConnector();
final String host = props.isHTTPSConfigured() ? props.getProperty(NiFiProperties.WEB_HTTPS_HOST) : props.getProperty(NiFiProperties.WEB_HTTP_HOST);
if (StringUtils.isNotBlank(host)) {
serverConnector.setHost(host);
}
server.addConnector(serverConnector);
} else {
interfaceNames.stream()
// Map interface name properties to Network Interfaces
.map(interfaceName -> {
try {
return NetworkInterface.getByName(interfaceName);
} catch (final SocketException e) {
throw new UncheckedIOException(String.format("Network Interface [%s] not found", interfaceName), e);
}
})
// Map Network Interfaces to host addresses
.filter(Objects::nonNull)
.flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream())
.map(InetAddress::getHostAddress)
// Map host addresses to Server Connectors
.map(host -> {
final ServerConnector serverConnector = serverConnectorFactory.getServerConnector();
serverConnector.setHost(host);
return serverConnector;
})
.forEach(server::addConnector);
}
} catch (final Throwable e) {
startUpFailure(e);
}
}

protected List<URI> getApplicationUrls() {
return Arrays.stream(server.getConnectors())
.map(connector -> (ServerConnector) connector)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.server;

import org.apache.nifi.util.NiFiProperties;
import org.eclipse.jetty.server.Server;

/**
* Abstraction for configuring Server instances based on application properties
*/
interface ServerProvider {
/**
* Get Server configured using application properties
*
* @param properties Application properties
* @return Server
*/
Server getServer(NiFiProperties properties);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.server;

import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.server.connector.FrameworkServerConnectorFactory;
import org.apache.nifi.web.server.handler.HeaderWriterHandler;
import org.apache.nifi.web.server.log.RequestLogProvider;
import org.apache.nifi.web.server.log.StandardRequestLogProvider;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.thread.QueuedThreadPool;

import javax.net.ssl.SSLContext;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Standard implementation of Server Provider with default Handlers
*/
class StandardServerProvider implements ServerProvider {
private static final String ALL_PATHS_PATTERN = "/*";

private static final String FRONTEND_CONTEXT_PATH = "/nifi";

private final SSLContext sslContext;

StandardServerProvider(final SSLContext sslContext) {
this.sslContext = sslContext;
}

@Override
public Server getServer(final NiFiProperties properties) {
Objects.requireNonNull(properties, "Properties required");

final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads());
threadPool.setName("NiFi Web Server");
final Server server = new Server(threadPool);
addConnectors(server, properties, sslContext);

final Handler standardHandler = getStandardHandler(properties);
server.setHandler(standardHandler);

final RewriteHandler defaultRewriteHandler = new RewriteHandler();
final RedirectPatternRule redirectDefault = new RedirectPatternRule(ALL_PATHS_PATTERN, FRONTEND_CONTEXT_PATH);
defaultRewriteHandler.addRule(redirectDefault);
server.setDefaultHandler(defaultRewriteHandler);

final String requestLogFormat = properties.getProperty(NiFiProperties.WEB_REQUEST_LOG_FORMAT);
final RequestLogProvider requestLogProvider = new StandardRequestLogProvider(requestLogFormat);
final RequestLog requestLog = requestLogProvider.getRequestLog();
server.setRequestLog(requestLog);

return server;
}

private void addConnectors(final Server server, final NiFiProperties properties, final SSLContext sslContext) {
final FrameworkServerConnectorFactory serverConnectorFactory = new FrameworkServerConnectorFactory(server, properties);
if (properties.isHTTPSConfigured()) {
serverConnectorFactory.setSslContext(sslContext);
}

final Map<String, String> interfaces = properties.isHTTPSConfigured() ? properties.getHttpsNetworkInterfaces() : properties.getHttpNetworkInterfaces();
final Set<String> interfaceNames = interfaces.values().stream().filter(StringUtils::isNotBlank).collect(Collectors.toSet());
// Add Server Connectors based on configured Network Interface Names
if (interfaceNames.isEmpty()) {
final ServerConnector serverConnector = serverConnectorFactory.getServerConnector();
final String host = properties.isHTTPSConfigured() ? properties.getProperty(NiFiProperties.WEB_HTTPS_HOST) : properties.getProperty(NiFiProperties.WEB_HTTP_HOST);
if (StringUtils.isNotBlank(host)) {
serverConnector.setHost(host);
}
server.addConnector(serverConnector);
} else {
interfaceNames.stream()
// Map interface name properties to Network Interfaces
.map(interfaceName -> {
try {
return NetworkInterface.getByName(interfaceName);
} catch (final SocketException e) {
throw new UncheckedIOException(String.format("Network Interface [%s] not found", interfaceName), e);
}
})
// Map Network Interfaces to host addresses
.filter(Objects::nonNull)
.flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream())
.map(InetAddress::getHostAddress)
// Map host addresses to Server Connectors
.map(host -> {
final ServerConnector serverConnector = serverConnectorFactory.getServerConnector();
serverConnector.setHost(host);
return serverConnector;
})
.forEach(server::addConnector);
}
}

private Handler getStandardHandler(final NiFiProperties properties) {
// Standard Handler supporting an ordered sequence of Handlers invoked until completion
final Handler.Collection standardHandler = new Handler.Sequence();

// Set Handler for standard response headers
standardHandler.addHandler(new HeaderWriterHandler());

// Validate Host Header when running with HTTPS enabled
if (properties.isHTTPSConfigured()) {
final HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(properties);
standardHandler.addHandler(hostHeaderHandler);
}

return standardHandler;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,9 @@
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlets.DoSFilter;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
import org.springframework.security.web.header.writers.HstsHeaderWriter;
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;

import jakarta.servlet.Filter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
Expand All @@ -45,8 +37,6 @@
public class StandardRequestFilterProvider implements RequestFilterProvider {
private static final int MAX_CONTENT_SIZE_DISABLED = 0;

private static final String STANDARD_CONTENT_POLICY = "frame-ancestors 'self'";

/**
* Get Filters using provided NiFi Properties
*
Expand All @@ -63,8 +53,6 @@ public List<FilterHolder> getFilters(final NiFiProperties properties) {
filters.add(getFilterHolder(RequestAuthenticationFilter.class));
}

filters.add(getHeaderWriterFilter());

final int maxContentSize = getMaxContentSize(properties);
if (maxContentSize > MAX_CONTENT_SIZE_DISABLED) {
final FilterHolder contentLengthFilter = getContentLengthFilter(maxContentSize);
Expand Down Expand Up @@ -93,21 +81,6 @@ protected FilterHolder getDenialOfServiceFilter(final NiFiProperties properties,
return filter;
}

private FilterHolder getHeaderWriterFilter() {
final List<HeaderWriter> headerWriters = Arrays.asList(
new ContentSecurityPolicyHeaderWriter(STANDARD_CONTENT_POLICY),
new HstsHeaderWriter(),
new XContentTypeOptionsHeaderWriter(),
new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN),
new XXssProtectionHeaderWriter()
);

final HeaderWriterFilter headerWriterFilter = new HeaderWriterFilter(headerWriters);
final FilterHolder filterHolder = new FilterHolder(headerWriterFilter);
filterHolder.setName(HeaderWriterFilter.class.getSimpleName());
return filterHolder;
}

private FilterHolder getFilterHolder(final Class<? extends Filter> filterClass) {
final FilterHolder filter = new FilterHolder(filterClass);
filter.setName(filterClass.getSimpleName());
Expand Down
Loading

0 comments on commit 5f72313

Please sign in to comment.