This is an automated email from the ASF dual-hosted git repository.
bbende pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new ae5a77b84f NIFI-14209 Restructured Host Header Validation (#9680)
ae5a77b84f is described below
commit ae5a77b84f5c7e5e51e85e99f1d40079dbdee5f1
Author: David Handermann <[email protected]>
AuthorDate: Wed Feb 5 13:43:07 2025 -0600
NIFI-14209 Restructured Host Header Validation (#9680)
- Replaced HostHeaderHandler with HostPortValidatorCustomizer
- Jetty SecureRequestCustomizer enforces host validation for SNI with
Server Certificate DNS Subject Alternative Names
- Added tests for TLS SNI with invalid host and port values
- Refactored and streamlined RequestUriBuilder.fromHttpServletRequest()
---
.../nifi/web/servlet/shared/RequestUriBuilder.java | 11 -
.../web/servlet/shared/RequestUriBuilderTest.java | 18 +-
.../src/main/asciidoc/administration-guide.adoc | 1 +
.../apache/nifi/web/server/HostHeaderHandler.java | 318 ---------------------
.../nifi/web/server/StandardServerProvider.java | 10 +-
.../connector/FrameworkServerConnectorFactory.java | 50 +++-
.../connector/HostPortValidatorCustomizer.java | 85 ++++++
.../nifi/web/server/HostHeaderHandlerTest.java | 139 ---------
.../web/server/StandardServerProviderTest.java | 154 +++++++++-
.../apache/nifi/web/api/ApplicationResource.java | 4 +-
.../nifi/web/api/TestApplicationResource.java | 9 +
.../nifi/web/api/TestDataTransferResource.java | 8 +
.../SamlAuthenticationSecurityConfiguration.java | 2 +-
.../StandardRelyingPartyRegistrationResolver.java | 9 +-
...andardRelyingPartyRegistrationResolverTest.java | 12 +-
15 files changed, 316 insertions(+), 514 deletions(-)
diff --git
a/nifi-commons/nifi-web-servlet-shared/src/main/java/org/apache/nifi/web/servlet/shared/RequestUriBuilder.java
b/nifi-commons/nifi-web-servlet-shared/src/main/java/org/apache/nifi/web/servlet/shared/RequestUriBuilder.java
index 2142c2dfe0..0bde71c3d4 100644
---
a/nifi-commons/nifi-web-servlet-shared/src/main/java/org/apache/nifi/web/servlet/shared/RequestUriBuilder.java
+++
b/nifi-commons/nifi-web-servlet-shared/src/main/java/org/apache/nifi/web/servlet/shared/RequestUriBuilder.java
@@ -59,17 +59,6 @@ public class RequestUriBuilder {
*/
public static RequestUriBuilder fromHttpServletRequest(final
HttpServletRequest httpServletRequest) {
final List<String> allowedContextPaths =
getAllowedContextPathsConfigured(httpServletRequest);
- return fromHttpServletRequest(httpServletRequest, allowedContextPaths);
- }
-
- /**
- * Return Builder from HTTP Servlet Request using Scheme, Host, Port, and
Context Path reading from headers
- *
- * @param httpServletRequest HTTP Servlet Request
- * @param allowedContextPaths Comma-separated string of allowed context
path values for proxy headers
- * @return Request URI Builder
- */
- public static RequestUriBuilder fromHttpServletRequest(final
HttpServletRequest httpServletRequest, final List<String> allowedContextPaths) {
final RequestUriProvider requestUriProvider = new
StandardRequestUriProvider(allowedContextPaths);
final URI requestUri =
requestUriProvider.getRequestUri(httpServletRequest);
return new RequestUriBuilder(requestUri.getScheme(),
requestUri.getHost(), requestUri.getPort(), requestUri.getPath());
diff --git
a/nifi-commons/nifi-web-servlet-shared/src/test/java/org/apache/nifi/web/servlet/shared/RequestUriBuilderTest.java
b/nifi-commons/nifi-web-servlet-shared/src/test/java/org/apache/nifi/web/servlet/shared/RequestUriBuilderTest.java
index c3d03a5dd4..f3a924c9ab 100644
---
a/nifi-commons/nifi-web-servlet-shared/src/test/java/org/apache/nifi/web/servlet/shared/RequestUriBuilderTest.java
+++
b/nifi-commons/nifi-web-servlet-shared/src/test/java/org/apache/nifi/web/servlet/shared/RequestUriBuilderTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.web.servlet.shared;
+import jakarta.servlet.ServletContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -23,8 +24,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
-import java.util.Collections;
-import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -48,16 +47,22 @@ public class RequestUriBuilderTest {
private static final String EMPTY = "";
+ private static final String ALLOWED_CONTEXT_PATHS = "allowedContextPaths";
+
@Mock
private HttpServletRequest httpServletRequest;
+ @Mock
+ private ServletContext servletContext;
+
@Test
public void testFromHttpServletRequestBuild() {
+
when(httpServletRequest.getServletContext()).thenReturn(servletContext);
when(httpServletRequest.getServerPort()).thenReturn(PORT);
when(httpServletRequest.getScheme()).thenReturn(SCHEME);
lenient().when(httpServletRequest.getHeader(eq(HOST_HEADER))).thenReturn(HOST);
- final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest,
Collections.emptyList());
+ final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
final URI uri = builder.build();
assertNotNull(uri);
@@ -69,11 +74,12 @@ public class RequestUriBuilderTest {
@Test
public void testFromHttpServletRequestPathBuild() {
+
when(httpServletRequest.getServletContext()).thenReturn(servletContext);
when(httpServletRequest.getServerPort()).thenReturn(PORT);
when(httpServletRequest.getScheme()).thenReturn(SCHEME);
lenient().when(httpServletRequest.getHeader(eq(HOST_HEADER))).thenReturn(HOST);
- final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest,
Collections.emptyList());
+ final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
builder.fragment(FRAGMENT).path(CONTEXT_PATH);
final URI uri = builder.build();
@@ -87,12 +93,14 @@ public class RequestUriBuilderTest {
@Test
public void testFromHttpServletRequestProxyHeadersBuild() {
+
when(httpServletRequest.getServletContext()).thenReturn(servletContext);
+
when(servletContext.getInitParameter(eq(ALLOWED_CONTEXT_PATHS))).thenReturn(CONTEXT_PATH);
when(httpServletRequest.getHeader(eq(ProxyHeader.PROXY_SCHEME.getHeader()))).thenReturn(SCHEME);
when(httpServletRequest.getHeader(eq(ProxyHeader.PROXY_HOST.getHeader()))).thenReturn(HOST);
when(httpServletRequest.getHeader(eq(ProxyHeader.PROXY_PORT.getHeader()))).thenReturn(Integer.toString(PORT));
when(httpServletRequest.getHeader(eq(ProxyHeader.PROXY_CONTEXT_PATH.getHeader()))).thenReturn(CONTEXT_PATH);
- final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest,
List.of(CONTEXT_PATH));
+ final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
final URI uri = builder.build();
assertNotNull(uri);
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 75a4030fcf..fe10d046b9 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -3444,6 +3444,7 @@ The value can be set to `h2` to require HTTP/2 and
disable HTTP/1.1.
|`nifi.web.proxy.host`|A comma separated list of allowed HTTP Host header
values to consider when NiFi is running securely and will be receiving requests
to a different host[:port] than it is bound to.
For example, when running in a Docker container or behind a proxy (e.g.
localhost:18443, proxyhost:443). By default, this value is blank meaning NiFi
should only allow requests sent to the
host[:port] that NiFi is bound to.
+Requests containing an invalid port in the Host or authority header return an
HTTP 421 Misdirected Request status.
|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP
X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header values to
consider. By default, this value is
blank meaning all requests containing a proxy context path are rejected.
Configuring this property would allow requests where the proxy path is
contained in this listing.
|`nifi.web.max.content.size`|The maximum size (HTTP `Content-Length`) for PUT
and POST requests. No default value is set for backward compatibility.
Providing a value for this property enables the `Content-Length` filter on all
incoming API requests (except Site-to-Site and cluster communications). A
suggested value is `20 MB`.
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/HostHeaderHandler.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/HostHeaderHandler.java
deleted file mode 100644
index 97337d63e2..0000000000
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/HostHeaderHandler.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * 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.commons.text.StringEscapeUtils;
-import org.apache.http.conn.util.InetAddressUtils;
-import org.apache.nifi.util.NiFiProperties;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.server.Handler;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Response;
-import org.eclipse.jetty.util.Callback;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.PrintWriter;
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-public class HostHeaderHandler extends Handler.Abstract {
- private static final Logger logger =
LoggerFactory.getLogger(HostHeaderHandler.class);
-
- private final String serverName;
- private final int serverPort;
- private final List<String> validHosts;
-
- /**
- * Instantiates a handler which accepts incoming requests with a host
header that is empty or contains one of the
- * valid hosts. See the Apache NiFi Admin Guide for instructions on how to
set valid hostnames and IP addresses.
- *
- * @param niFiProperties the NiFiProperties
- */
- public HostHeaderHandler(final NiFiProperties niFiProperties) {
- this.serverName =
Objects.requireNonNull(determineServerHostname(niFiProperties));
- this.serverPort = determineServerPort(niFiProperties);
-
- // Default values across generic instances
- List<String> hosts = generateDefaultHostnames(niFiProperties);
-
- // The value from nifi.web.http|https.host
- hosts.add(serverName.toLowerCase());
- hosts.add(serverName.toLowerCase() + ":" + serverPort);
-
- // The value(s) from nifi.web.proxy.host
- hosts.addAll(parseCustomHostnames(niFiProperties));
-
- // empty is ok here
- hosts.add("");
-
- this.validHosts = uniqueList(hosts);
- logger.info("{} valid values for HTTP Request Host Header: {}",
validHosts.size(), StringUtils.join(validHosts, ", "));
- }
-
- /**
- * Returns the list of parsed custom hostnames from {@code
nifi.web.proxy.host} in {@link NiFiProperties}.
- * This list is deduplicated (if a host {@code somehost.com:1234} is
provided, it will show twice, as the "portless"
- * version {@code somehost.com} is also generated). IPv6 addresses are
only modified if they adhere to the strict
- * formatting using {@code []} around the address as specified in RFC 5952
Section 6 (i.e.
- * {@code [1234.5678.90AB.CDEF.1234.5678.90AB.CDEF]:1234} will insert
- * {@code [1234.5678.90AB.CDEF.1234.5678.90AB.CDEF]} as well).
- *
- * @param niFiProperties the properties object
- * @return the list of parsed custom hostnames
- */
- List<String> parseCustomHostnames(NiFiProperties niFiProperties) {
- // Load the custom hostnames from the properties
- List<String> customHostnames = niFiProperties.getAllowedHostsAsList();
-
- /* Each IPv4 address and hostname may have the port associated, so
duplicate the list and trim the port
- * (the port may be different from the port NiFi is running on if
provided by a proxy, etc.) IPv6 addresses
- * are not modified.
- */
- List<String> portlessHostnames = customHostnames.stream().map(hostname
-> {
- if (isIPv6Address(hostname)) {
- return hostname;
- } else {
- return StringUtils.substringBeforeLast(hostname, ":");
- }
- }
- ).collect(Collectors.toList());
-
- customHostnames.addAll(portlessHostnames);
- if (logger.isDebugEnabled()) {
- logger.debug("Parsed {} custom hostnames from nifi.web.proxy.host:
{}", customHostnames.size(), StringUtils.join(customHostnames, ", "));
- }
- return uniqueList(customHostnames);
- }
-
- /**
- * Returns a unique {@code List} of the elements maintaining the original
order.
- *
- * @param duplicateList a list that may contain duplicate elements
- * @return a list maintaining the original order which no longer contains
duplicate elements
- */
- private static List<String> uniqueList(List<String> duplicateList) {
- return new ArrayList<>(new LinkedHashSet<>(duplicateList));
- }
-
- /**
- * Returns true if the provided address is an IPv6 address (or could be
interpreted as one). This method is more
- * lenient than {@link InetAddressUtils#isIPv6Address(String)} because of
different interpretations of IPv4-mapped
- * IPv6 addresses.
- * See RFC 5952 Section 4 for more information on textual representation
of the IPv6 addresses.
- *
- * @param address the address in text form
- * @return true if the address is or could be parsed as an IPv6 address
- */
- static boolean isIPv6Address(String address) {
- // Note: InetAddressUtils#isIPv4MappedIPv64Address() fails on
addresses that do not compress the leading 0:0:0... to ::
- // Expanded for debugging purposes
- boolean isNormalIPv6 = InetAddressUtils.isIPv6Address(address);
-
- // If the last two hextets are written in IPv4 form, treat it as an
IPv6 address as well
- String everythingAfterLastColon =
StringUtils.substringAfterLast(address, ":");
- boolean isIPv4 =
InetAddressUtils.isIPv4Address(everythingAfterLastColon);
-
- return isNormalIPv6 || isIPv4;
- }
-
- private int determineServerPort(NiFiProperties props) {
- return props.getSslPort() != null ? props.getSslPort() :
props.getPort();
- }
-
- private String determineServerHostname(NiFiProperties props) {
- if (props.getSslPort() != null) {
- return props.getProperty(NiFiProperties.WEB_HTTPS_HOST,
"localhost");
- } else {
- return props.getProperty(NiFiProperties.WEB_HTTP_HOST,
"localhost");
- }
- }
-
- /**
- * Host Header Valid status checks against valid hosts
- *
- * @param hostHeader Host header value
- * @return Valid status
- */
- boolean hostHeaderIsValid(final String hostHeader) {
- return hostHeader != null &&
validHosts.contains(hostHeader.toLowerCase().trim());
- }
-
- @Override
- public String toString() {
- return "HostHeaderHandler for " + serverName + ":" + serverPort;
- }
-
- /**
- * Returns an error message to the response and marks the request as
handled if the host header is not valid.
- * Otherwise passes the request on to the next scoped handler.
- *
- * @param request the request as an HttpServletRequest
- * @param response the current response
- */
- @Override
- public boolean handle(final Request request, Response response, Callback
callback) {
- final String hostHeader = request.getHeaders().get(HttpHeader.HOST);
- final String requestUri = request.getHttpURI().asString();
- logger.debug("Request URI [{}] Host Header [{}]", requestUri,
hostHeader);
-
- if (!hostHeaderIsValid(hostHeader)) {
- logger.warn("Request URI [{}] Host Header [{}] not valid",
requestUri, hostHeader);
-
- response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;
charset=utf-8");
- response.setStatus(HttpURLConnection.HTTP_OK);
-
- try (PrintWriter out = Response.as(response, PrintWriter.class)) {
-
- out.println("<h1>System Error</h1>");
- out.println("<h2>The request contained an invalid host header
[<code>" + StringEscapeUtils.escapeHtml4(hostHeader) +
- "</code>] in the request [<code>" +
StringEscapeUtils.escapeHtml4(request.getHttpURI().asString()) +
- "</code>]. Check for request manipulation or
third-party intercept.</h2>");
- out.println("<h3>Valid host headers are [<code>empty</code>]
or: <br/><code>");
- out.println(printValidHosts());
- out.println("</code></h3>");
- }
-
- return true;
- } else {
- return false;
- }
- }
-
- String printValidHosts() {
- StringBuilder sb = new StringBuilder("<ul>");
- for (String vh : validHosts) {
- if (StringUtils.isNotBlank(vh))
-
sb.append("<li>").append(StringEscapeUtils.escapeHtml4(vh)).append("</li>\n");
- }
- return sb.append("</ul>\n").toString();
- }
-
- public static List<String> generateDefaultHostnames(NiFiProperties
niFiProperties) {
- List<String> validHosts = new ArrayList<>();
- int serverPort = 0;
-
- if (niFiProperties == null) {
- logger.warn("NiFiProperties not configured; returning minimal
default hostnames");
- } else {
- try {
- serverPort = niFiProperties.getConfiguredHttpOrHttpsPort();
- } catch (RuntimeException e) {
- logger.warn("Cannot fully generate list of default hostnames
because the server port is not configured in nifi.properties. Defaulting to
port 0 for host header evaluation");
- }
-
- // Add any custom network interfaces
- try {
- final int lambdaPort = serverPort;
- List<String> customIPs =
extractIPsFromNetworkInterfaces(niFiProperties);
- customIPs.forEach(ip -> {
- validHosts.add(ip);
- validHosts.add(ip + ":" + lambdaPort);
- });
- } catch (final Exception e) {
- logger.warn("Failed to determine custom network interfaces.",
e);
- }
- }
-
- // Sometimes the hostname is left empty but the port is always
populated
- validHosts.add("127.0.0.1");
- validHosts.add("127.0.0.1:" + serverPort);
- validHosts.add("localhost");
- validHosts.add("localhost:" + serverPort);
- validHosts.add("[::1]");
- validHosts.add("[::1]:" + serverPort);
-
- // Add the loopback and actual IP address and hostname used
- try {
-
validHosts.add(InetAddress.getLoopbackAddress().getHostAddress().toLowerCase());
-
validHosts.add(InetAddress.getLoopbackAddress().getHostAddress().toLowerCase()
+ ":" + serverPort);
-
-
validHosts.add(InetAddress.getLocalHost().getHostName().toLowerCase());
-
validHosts.add(InetAddress.getLocalHost().getHostName().toLowerCase() + ":" +
serverPort);
-
-
validHosts.add(InetAddress.getLocalHost().getHostAddress().toLowerCase());
-
validHosts.add(InetAddress.getLocalHost().getHostAddress().toLowerCase() + ":"
+ serverPort);
- } catch (final Exception e) {
- logger.warn("Failed to determine local hostname.", e);
- }
-
- // Dedupe but maintain order
- final List<String> uniqueHosts = uniqueList(validHosts);
- if (logger.isDebugEnabled()) {
- logger.debug("Determined {} valid default hostnames and IP
addresses for incoming headers: {}", uniqueHosts.size(),
StringUtils.join(uniqueHosts, ", "));
- }
- return uniqueHosts;
- }
-
- /**
- * Extracts the list of IP addresses from custom bound network interfaces.
If both HTTPS and HTTP interfaces are
- * defined and HTTPS is enabled, only HTTPS interfaces will be returned.
If none are defined, an empty list will be
- * returned.
- *
- * @param niFiProperties the NiFiProperties object
- * @return the list of IP addresses
- */
- static List<String> extractIPsFromNetworkInterfaces(NiFiProperties
niFiProperties) {
- Map<String, String> networkInterfaces =
niFiProperties.isHTTPSConfigured() ? niFiProperties.getHttpsNetworkInterfaces()
: niFiProperties.getHttpNetworkInterfaces();
- if (isNotDefined(networkInterfaces)) {
- // No custom interfaces defined
- return List.of();
- } else {
- final List<String> allIPAddresses = new ArrayList<>();
- for (Map.Entry<String, String> entry :
networkInterfaces.entrySet()) {
- final String networkInterfaceName = entry.getValue();
- try {
- final NetworkInterface ni =
NetworkInterface.getByName(networkInterfaceName);
- if (ni == null) {
- logger.warn("Cannot resolve network interface named
{}", networkInterfaceName);
- } else {
- final List<String> ipAddresses =
Collections.list(ni.getInetAddresses()).stream().map(inetAddress ->
inetAddress.getHostAddress().toLowerCase()).collect(Collectors.toList());
- logger.debug("Resolved the following IP addresses for
network interface {}: {}", networkInterfaceName, StringUtils.join(ipAddresses,
", "));
- allIPAddresses.addAll(ipAddresses);
- }
- } catch (SocketException e) {
- logger.warn("Cannot resolve network interface named {}",
networkInterfaceName);
- }
- }
-
- // Dedupe while maintaining order
- return uniqueList(allIPAddresses);
- }
- }
-
- /**
- * Returns true if the provided map of properties and network interfaces
is null, empty, or the actual definitions are empty.
- *
- * @param networkInterfaces the map of properties to bindings
- * ({@code
["nifi.web.http.network.interface.first":"eth0"]})
- * @return Not Defined status
- */
- static boolean isNotDefined(Map<String, String> networkInterfaces) {
- return networkInterfaces == null || networkInterfaces.isEmpty() ||
networkInterfaces.values().stream().filter(value ->
StringUtils.isNotBlank(value)).collect(Collectors.toList()).isEmpty();
- }
-}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/StandardServerProvider.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/StandardServerProvider.java
index 113e05ee64..492ea6071e 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/StandardServerProvider.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/StandardServerProvider.java
@@ -66,7 +66,7 @@ class StandardServerProvider implements ServerProvider {
final Server server = new Server(threadPool);
addConnectors(server, properties, sslContext);
- final Handler standardHandler = getStandardHandler(properties);
+ final Handler standardHandler = getStandardHandler();
server.setHandler(standardHandler);
final RewriteHandler defaultRewriteHandler = new RewriteHandler();
@@ -123,19 +123,13 @@ class StandardServerProvider implements ServerProvider {
}
}
- private Handler getStandardHandler(final NiFiProperties properties) {
+ private Handler getStandardHandler() {
// 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;
}
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
index 7da24b5ec6..ec1bee66fb 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
@@ -22,7 +22,6 @@ import
org.apache.nifi.jetty.configuration.connector.ApplicationLayerProtocol;
import
org.apache.nifi.jetty.configuration.connector.StandardServerConnectorFactory;
import org.apache.nifi.processor.DataUnit;
import org.apache.nifi.security.util.TlsPlatform;
-import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.eclipse.jetty.server.HostHeaderCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
@@ -30,28 +29,33 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Framework extension of Server Connector Factory configures additional
settings based on application properties
*/
public class FrameworkServerConnectorFactory extends
StandardServerConnectorFactory {
- private static final String DEFAULT_AUTO_REFRESH_INTERVAL = "30 s";
-
- private static final int IDLE_TIMEOUT_MULTIPLIER = 2;
+ private static final int IDLE_TIMEOUT = 60000;
private static final String CIPHER_SUITE_SEPARATOR_PATTERN = ",\\s*";
- private final int headerSize;
+ private static final Pattern HOST_PORT_PATTERN =
Pattern.compile(".+?:(\\d+)$");
- private final int idleTimeout;
+ private static final int PORT_GROUP = 1;
+
+ private final int headerSize;
private final String includeCipherSuites;
private final String excludeCipherSuites;
+ private final Set<Integer> validPorts;
+
private SslContextFactory.Server sslContextFactory;
/**
@@ -66,7 +70,7 @@ public class FrameworkServerConnectorFactory extends
StandardServerConnectorFact
includeCipherSuites =
properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE);
excludeCipherSuites =
properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE);
headerSize = DataUnit.parseDataSize(properties.getWebMaxHeaderSize(),
DataUnit.B).intValue();
- idleTimeout = getIdleTimeout();
+ validPorts = getValidPorts(properties);
if (properties.isHTTPSConfigured()) {
if (properties.isClientAuthRequiredForRestApi()) {
@@ -93,11 +97,14 @@ public class FrameworkServerConnectorFactory extends
StandardServerConnectorFact
httpConfiguration.setRequestHeaderSize(headerSize);
httpConfiguration.setResponseHeaderSize(headerSize);
- httpConfiguration.setIdleTimeout(idleTimeout);
+ httpConfiguration.setIdleTimeout(IDLE_TIMEOUT);
// Add HostHeaderCustomizer to set Host Header for HTTP/2 and
HostHeaderHandler
httpConfiguration.addCustomizer(new HostHeaderCustomizer());
+ final HostPortValidatorCustomizer hostPortValidatorCustomizer = new
HostPortValidatorCustomizer(validPorts);
+ httpConfiguration.addCustomizer(hostPortValidatorCustomizer);
+
return httpConfiguration;
}
@@ -135,12 +142,6 @@ public class FrameworkServerConnectorFactory extends
StandardServerConnectorFact
setApplicationLayerProtocols(applicationLayerProtocols);
}
- private int getIdleTimeout() {
- final String autoRefreshInterval = DEFAULT_AUTO_REFRESH_INTERVAL;
- final double autoRefreshMilliseconds =
FormatUtils.getPreciseTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS);
- return Math.multiplyExact((int) autoRefreshMilliseconds,
IDLE_TIMEOUT_MULTIPLIER);
- }
-
private String[] getCipherSuites(final String cipherSuitesProperty) {
return cipherSuitesProperty.split(CIPHER_SUITE_SEPARATOR_PATTERN);
}
@@ -157,4 +158,23 @@ public class FrameworkServerConnectorFactory extends
StandardServerConnectorFact
return ObjectUtils.defaultIfNull(httpsPort, httpPort);
}
+
+ private static Set<Integer> getValidPorts(final NiFiProperties properties)
{
+ final Set<Integer> validPorts = new HashSet<>();
+
+ final int serverPort = getPort(properties);
+ validPorts.add(serverPort);
+
+ final List<String> allowedHosts = properties.getAllowedHostsAsList();
+ for (final String allowedHost : allowedHosts) {
+ final Matcher portMatcher = HOST_PORT_PATTERN.matcher(allowedHost);
+ if (portMatcher.matches()) {
+ final String portGroup = portMatcher.group(PORT_GROUP);
+ final int allowedPort = Integer.parseInt(portGroup);
+ validPorts.add(allowedPort);
+ }
+ }
+
+ return validPorts;
+ }
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/HostPortValidatorCustomizer.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/HostPortValidatorCustomizer.java
new file mode 100644
index 0000000000..d36304e71a
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/HostPortValidatorCustomizer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.connector;
+
+import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.server.ConnectionMetaData;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.Request;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Jetty Request Customizer implementing validation of port included in
HTTP/1.1 Host Header or HTTP/2 authority header
+ */
+public class HostPortValidatorCustomizer implements
HttpConfiguration.Customizer {
+ private static final String MISDIRECTED_REQUEST_REASON = "Invalid Port
Requested";
+
+ private static final int PORT_NOT_SPECIFIED = -1;
+
+ private final Set<Integer> validPorts;
+
+ /**
+ * HOst Port Validator Customer constructor with additional valid ports
from application properties
+ *
+ * @param validPorts Valid Ports on HTTPS requests
+ */
+ public HostPortValidatorCustomizer(final Set<Integer> validPorts) {
+ this.validPorts = Objects.requireNonNull(validPorts, "Valid Ports
required");
+ }
+
+ /**
+ * Validate requested port against connected port and valid ports for
secure HTTPS requests.
+ * The port is not specified when the header includes only the domain name
as described in RFC 9110 Section 7.2.
+ * The port must match the local socket address port or a configured valid
port number.
+ *
+ * @param request HTTP Request to be evaluated
+ * @param responseHeaders HTTP Response headers
+ * @return Valid HTTP Request
+ */
+ @Override
+ public Request customize(final Request request, final HttpFields.Mutable
responseHeaders) {
+ final Request customized;
+
+ if (request.isSecure()) {
+ final HttpURI requestUri = request.getHttpURI();
+ final int port = requestUri.getPort();
+ final int localSocketAddressPort =
getLocalSocketAddressPort(request);
+
+ if (PORT_NOT_SPECIFIED == port || localSocketAddressPort == port
|| validPorts.contains(port)) {
+ customized = request;
+ } else {
+ throw new
BadMessageException(HttpStatus.MISDIRECTED_REQUEST_421,
MISDIRECTED_REQUEST_REASON);
+ }
+ } else {
+ customized = request;
+ }
+
+ return customized;
+ }
+
+ private int getLocalSocketAddressPort(final Request request) {
+ final ConnectionMetaData connectionMetaData =
request.getConnectionMetaData();
+ final InetSocketAddress localSocketAddress = (InetSocketAddress)
connectionMetaData.getLocalSocketAddress();
+ return localSocketAddress.getPort();
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.java
deleted file mode 100644
index b87fbef5f7..0000000000
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Properties;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class HostHeaderHandlerTest {
- private static final String DEFAULT_HOSTNAME = "nifi.apache.org";
- private static final int DEFAULT_PORT = 8080;
- private static final List<String> IPV6_HOSTS =
Arrays.asList("ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
- "2001:DB8:0:0:8:800:200C:417A",
- "FF01:0:0:0:0:0:0:101",
- "0:0:0:0:0:0:0:1",
- "0:0:0:0:0:0:0:0",
- "2001:DB8::8:800:200C:417A",
- "FF01::101",
- "::1",
- "::",
- "0:0:0:0:0:0:13.1.68.3",
- "0:0:0:0:0:FFFF:129.144.52.38",
- "::13.1.68.3",
- "FFFF:129.144.52.38",
- "::FFFF:129.144.52.38");
-
- private static List<String> defaultHostsAndPorts;
-
- @BeforeAll
- public static void setUpOnce() throws Exception {
- String actualHostname =
InetAddress.getLocalHost().getHostName().toLowerCase();
- List<String> defaultHosts150 = Arrays.asList(DEFAULT_HOSTNAME,
"localhost", actualHostname);
- String actualIp = InetAddress.getLocalHost().getHostAddress();
- String loopbackIp = InetAddress.getLoopbackAddress().getHostAddress();
- List<String> defaultHosts = new ArrayList<>(defaultHosts150);
- defaultHosts.remove(DEFAULT_HOSTNAME);
- defaultHosts.addAll(Arrays.asList("[::1]", "127.0.0.1", actualIp,
loopbackIp));
- defaultHostsAndPorts = buildHostsWithPorts(defaultHosts, DEFAULT_PORT);
- }
-
- @Test
- public void testNewConstructorShouldHandleCurrentDefaultValues() {
- HostHeaderHandler handler = new
HostHeaderHandler(getNifiProperties(null));
-
- defaultHostsAndPorts.forEach(host ->
assertTrue(handler.hostHeaderIsValid(host)));
- }
-
- @Test
- public void testShouldParseCustomHostnames() {
- List<String> otherHosts = Arrays.asList("someotherhost.com:9999",
"yetanotherbadhost.com", "10.10.10.1:1234", "100.100.100.1");
- NiFiProperties nifiProperties = getNifiProperties(otherHosts);
- HostHeaderHandler handler = new HostHeaderHandler(nifiProperties);
- final List<String> customHostnames =
handler.parseCustomHostnames(nifiProperties);
-
- assertEquals(otherHosts.size() + 2, customHostnames.size()); // Two
provided hostnames had ports
- otherHosts.forEach(host -> {
- assertTrue(customHostnames.contains(host));
- String portlessHost = host.split(":", 2)[0];
- assertTrue(customHostnames.contains(portlessHost));
- });
- }
-
- @Test
- public void testParseCustomHostnamesShouldHandleIPv6WithoutPorts() {
- NiFiProperties nifiProperties = getNifiProperties(IPV6_HOSTS);
- HostHeaderHandler handler = new HostHeaderHandler(nifiProperties);
- List<String> customHostnames =
handler.parseCustomHostnames(nifiProperties);
-
- assertEquals(IPV6_HOSTS.size(), customHostnames.size());
- IPV6_HOSTS.forEach(host -> assertTrue(customHostnames.contains(host)));
- }
-
- @Test
- public void testParseCustomHostnamesShouldHandleIPv6WithPorts() {
- int port = 1234;
- List<String> ipv6HostsWithPorts =
buildHostsWithPorts(IPV6_HOSTS.stream()
- .map(host -> "[" + host + "]")
- .collect(Collectors.toList()), port);
- NiFiProperties nifiProperties = getNifiProperties(ipv6HostsWithPorts);
- HostHeaderHandler handler = new HostHeaderHandler(nifiProperties);
- List<String> customHostnames =
handler.parseCustomHostnames(nifiProperties);
-
- assertEquals(ipv6HostsWithPorts.size() * 2, customHostnames.size());
- ipv6HostsWithPorts.forEach(host -> {
- assertTrue(customHostnames.contains(host));
- String portlessHost =
StringUtils.substringBeforeLast(host, ":");
- assertTrue(customHostnames.contains(portlessHost));
- }
- );
- }
-
- @Test
- public void testShouldIdentifyIPv6Addresses() {
- IPV6_HOSTS.forEach(host ->
assertTrue(HostHeaderHandler.isIPv6Address(host)));
- }
-
- private static List<String> buildHostsWithPorts(List<String> hosts, int
port) {
- return hosts.stream()
- .map(host -> host + ":" + port)
- .collect(Collectors.toList());
- }
-
- private NiFiProperties getNifiProperties(List<String> hosts) {
- Properties bareboneProperties = new Properties();
- bareboneProperties.put(NiFiProperties.WEB_HTTPS_HOST,
DEFAULT_HOSTNAME);
- bareboneProperties.put(NiFiProperties.WEB_HTTPS_PORT,
Integer.toString(DEFAULT_PORT));
-
- if (hosts != null) {
- bareboneProperties.put(NiFiProperties.WEB_PROXY_HOST,
String.join(",", hosts));
- }
-
- return new NiFiProperties(bareboneProperties);
- }
-}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
index 093fb0ffae..f0d9b193cb 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
@@ -17,24 +17,44 @@
package org.apache.nifi.web.server;
import org.apache.nifi.jetty.configuration.connector.ApplicationLayerProtocol;
+import org.apache.nifi.security.cert.builder.StandardCertificateBuilder;
+import org.apache.nifi.security.ssl.EphemeralKeyStoreBuilder;
+import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.server.handler.HeaderWriterHandler;
+import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.springframework.web.util.UriComponentsBuilder;
import javax.net.ssl.SSLContext;
-import java.security.NoSuchAlgorithmException;
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
import java.util.List;
import java.util.Properties;
+import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class StandardServerProviderTest {
@@ -42,6 +62,48 @@ class StandardServerProviderTest {
private static final String SSL_PROTOCOL = "ssl";
+ private static final Duration TIMEOUT = Duration.ofSeconds(15);
+
+ private static final String ALIAS = "entry-0";
+
+ private static final char[] PROTECTION_PARAMETER = new char[]{};
+
+ private static final String LOCALHOST_NAME = "localhost";
+
+ private static final X500Principal LOCALHOST_SUBJECT = new
X500Principal("CN=%s, O=NiFi".formatted(LOCALHOST_NAME));
+
+ private static final String LOCALHOST_ADDRESS = "127.0.0.1";
+
+ private static final String LOCALHOST_HTTP_PORT = "localhost:80";
+
+ private static final String HOST_HEADER = "Host";
+
+ private static final String PUBLIC_HOST = "nifi.apache.org";
+
+ private static final String PUBLIC_UNKNOWN_HOST = "nifi.staged.apache.org";
+
+ private static final String ALLOW_RESTRICTED_HEADERS_PROPERTY =
"jdk.httpclient.allowRestrictedHeaders";
+
+ private static SSLContext sslContext;
+
+ @BeforeAll
+ static void setConfiguration() throws Exception {
+ final KeyPair keyPair =
KeyPairGenerator.getInstance("RSA").generateKeyPair();
+ final X509Certificate certificate = new
StandardCertificateBuilder(keyPair, LOCALHOST_SUBJECT, Duration.ofHours(1))
+ .setDnsSubjectAlternativeNames(List.of(PUBLIC_HOST))
+ .build();
+ final KeyStore keyStore = new EphemeralKeyStoreBuilder().build();
+ keyStore.setKeyEntry(ALIAS, keyPair.getPrivate(),
PROTECTION_PARAMETER, new Certificate[]{certificate});
+ sslContext = new StandardSslContextBuilder()
+ .keyStore(keyStore)
+ .trustStore(keyStore)
+ .keyPassword(PROTECTION_PARAMETER)
+ .build();
+
+ // Allow Restricted Headers for testing TLS SNI
+ System.setProperty(ALLOW_RESTRICTED_HEADERS_PROPERTY, HOST_HEADER);
+ }
+
@Test
void testGetServer() {
final Properties applicationProperties = new Properties();
@@ -57,12 +119,11 @@ class StandardServerProviderTest {
}
@Test
- void testGetServerHttps() throws NoSuchAlgorithmException {
+ void testGetServerHttps() {
final Properties applicationProperties = new Properties();
applicationProperties.setProperty(NiFiProperties.WEB_HTTPS_PORT,
RANDOM_PORT);
final NiFiProperties properties =
NiFiProperties.createBasicNiFiProperties(null, applicationProperties);
- final SSLContext sslContext = SSLContext.getDefault();
final StandardServerProvider provider = new
StandardServerProvider(sslContext);
final Server server = provider.getServer(properties);
@@ -93,6 +154,93 @@ class StandardServerProviderTest {
}
}
+ @Timeout(15)
+ @Test
+ void testGetServerHttpsRequestsCompleted() throws Exception {
+ final Properties applicationProperties = new Properties();
+ applicationProperties.setProperty(NiFiProperties.WEB_HTTPS_PORT,
RANDOM_PORT);
+ applicationProperties.setProperty(NiFiProperties.WEB_PROXY_HOST,
PUBLIC_HOST);
+ final NiFiProperties properties =
NiFiProperties.createBasicNiFiProperties(null, applicationProperties);
+
+ final StandardServerProvider provider = new
StandardServerProvider(sslContext);
+
+ final Server server = provider.getServer(properties);
+
+ assertStandardConfigurationFound(server);
+ assertHttpsConnectorFound(server);
+
+ try {
+ server.start();
+
+ assertFalse(server.isFailed());
+
+ while (server.isStarting()) {
+ TimeUnit.MILLISECONDS.sleep(250);
+ }
+
+ assertTrue(server.isStarted());
+
+ final URI uri = server.getURI();
+ assertHttpsRequestsCompleted(uri);
+ } finally {
+ server.stop();
+ }
+ }
+
+ void assertHttpsRequestsCompleted(final URI serverUri) throws IOException,
InterruptedException {
+ try (HttpClient httpClient = HttpClient.newBuilder()
+ .connectTimeout(TIMEOUT)
+ .sslContext(sslContext)
+ .build()
+ ) {
+ final URI localhostUri =
UriComponentsBuilder.fromUri(serverUri).host(LOCALHOST_NAME).build().toUri();
+
+ assertRedirectRequestsCompleted(httpClient, localhostUri);
+ assertBadRequestsCompleted(httpClient, localhostUri);
+ assertMisdirectedRequestsCompleted(httpClient, localhostUri);
+ }
+ }
+
+ void assertRedirectRequestsCompleted(final HttpClient httpClient, final
URI localhostUri) throws IOException, InterruptedException {
+ final HttpRequest localhostRequest =
HttpRequest.newBuilder(localhostUri)
+ .version(HttpClient.Version.HTTP_2)
+ .build();
+ assertResponseStatusCode(httpClient, localhostRequest,
HttpStatus.MOVED_TEMPORARILY_302);
+
+ final HttpRequest alternativeNameRequest =
HttpRequest.newBuilder(localhostUri)
+ .version(HttpClient.Version.HTTP_1_1)
+ .header(HOST_HEADER, PUBLIC_HOST)
+ .build();
+ assertResponseStatusCode(httpClient, alternativeNameRequest,
HttpStatus.MOVED_TEMPORARILY_302);
+ }
+
+ void assertBadRequestsCompleted(final HttpClient httpClient, final URI
localhostUri) throws IOException, InterruptedException {
+ final HttpRequest publicHostHeaderRequest =
HttpRequest.newBuilder(localhostUri)
+ .header(HOST_HEADER, PUBLIC_UNKNOWN_HOST)
+ .version(HttpClient.Version.HTTP_1_1)
+ .build();
+ assertResponseStatusCode(httpClient, publicHostHeaderRequest,
HttpStatus.BAD_REQUEST_400);
+
+ final HttpRequest localhostAddressRequest =
HttpRequest.newBuilder(localhostUri)
+ .header(HOST_HEADER, LOCALHOST_ADDRESS)
+ .version(HttpClient.Version.HTTP_1_1)
+ .build();
+ assertResponseStatusCode(httpClient, localhostAddressRequest,
HttpStatus.BAD_REQUEST_400);
+ }
+
+ void assertMisdirectedRequestsCompleted(final HttpClient httpClient, final
URI localhostUri) throws IOException, InterruptedException {
+ final HttpRequest localhostPortRequest =
HttpRequest.newBuilder(localhostUri)
+ .version(HttpClient.Version.HTTP_1_1)
+ .header(HOST_HEADER, LOCALHOST_HTTP_PORT)
+ .build();
+ assertResponseStatusCode(httpClient, localhostPortRequest,
HttpStatus.MISDIRECTED_REQUEST_421);
+ }
+
+ void assertResponseStatusCode(final HttpClient httpClient, final
HttpRequest request, final int statusCodeExpected) throws IOException,
InterruptedException {
+ final HttpResponse<Void> response = httpClient.send(request,
HttpResponse.BodyHandlers.discarding());
+ assertEquals(statusCodeExpected, response.statusCode());
+ }
+
void assertHttpConnectorFound(final Server server) {
final Connector[] connectors = server.getConnectors();
assertNotNull(connectors);
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index 1bf392cff1..5b48502d11 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -172,7 +172,7 @@ public abstract class ApplicationResource {
* @return the full external UI
*/
protected String generateExternalUiUri(final String... pathSegments) {
- final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest,
properties.getAllowedContextPathsAsList());
+ final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
final String path = String.join("/", pathSegments);
builder.path(path);
@@ -186,7 +186,7 @@ public abstract class ApplicationResource {
}
private URI buildResourceUri(final URI uri) {
- final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest,
properties.getAllowedContextPathsAsList());
+ final RequestUriBuilder builder =
RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
builder.path(uri.getPath());
return builder.build();
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
index 23cb92b2fd..c0bed9a91f 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.web.api;
+import jakarta.servlet.ServletContext;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.servlet.shared.ProxyHeader;
import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
@@ -38,6 +39,7 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@@ -54,10 +56,14 @@ public class TestApplicationResource {
private static final String ACTUAL_RESOURCE = "actualResource";
private static final String EXPECTED_URI = BASE_URI + ":" + PORT +
ALLOWED_PATH + FORWARD_SLASH + ACTUAL_RESOURCE;
private static final String MULTIPLE_ALLOWED_PATHS = String.join(",",
ALLOWED_PATH, "another/path", "a/third/path");
+ private static final String ALLOWED_CONTEXT_PATHS = "allowedContextPaths";
@Mock
private HttpServletRequest request;
+ @Mock
+ private ServletContext servletContext;
+
private MockApplicationResource resource;
@BeforeEach
@@ -69,6 +75,8 @@ public class TestApplicationResource {
when(request.getServerName()).thenReturn(HOST);
when(request.getServerPort()).thenReturn(PORT);
+ when(request.getServletContext()).thenReturn(servletContext);
+
resource = new MockApplicationResource();
resource.setHttpServletRequest(request);
resource.setUriInfo(uriInfo);
@@ -156,6 +164,7 @@ public class TestApplicationResource {
private void setNiFiProperties(Map<String, String> props) {
resource.properties = new NiFiProperties(props);
+
when(servletContext.getInitParameter(eq(ALLOWED_CONTEXT_PATHS))).thenReturn(resource.properties.getAllowedContextPaths());
}
private static class MockApplicationResource extends ApplicationResource {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
index ce4dfb17d7..851a9acfa2 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
@@ -49,6 +49,7 @@ import java.net.URL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
@@ -62,6 +63,7 @@ public class TestDataTransferResource {
@BeforeAll
public static void setup() throws Exception {
final URL resource =
TestDataTransferResource.class.getResource("/site-to-site/nifi.properties");
+ assertNotNull(resource);
final String propertiesFile = resource.toURI().getPath();
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH,
propertiesFile);
}
@@ -72,6 +74,8 @@ public class TestDataTransferResource {
doReturn(new StringBuffer("http://nifi.example.com:8080")
.append("/nifi-api/data-transfer/output-ports/port-id/transactions/tx-id/flow-files"))
.when(req).getRequestURL();
+ final ServletContext servletContext = mock(ServletContext.class);
+ when(req.getServletContext()).thenReturn(servletContext);
return req;
}
@@ -174,6 +178,8 @@ public class TestDataTransferResource {
.getDeclaredField("httpServletRequest");
httpServletRequestField.setAccessible(true);
httpServletRequestField.set(resource, request);
+ final ServletContext servletContext = mock(ServletContext.class);
+ when(request.getServletContext()).thenReturn(servletContext);
final InputStream inputStream = null;
@@ -209,6 +215,8 @@ public class TestDataTransferResource {
.getDeclaredField("httpServletRequest");
httpServletRequestField.setAccessible(true);
httpServletRequestField.set(resource, request);
+ final ServletContext servletContext = mock(ServletContext.class);
+ when(request.getServletContext()).thenReturn(servletContext);
final InputStream inputStream = null;
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java
index 55052a6c9e..e644f8e3e4 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java
@@ -303,7 +303,7 @@ public class SamlAuthenticationSecurityConfiguration {
*/
@Bean
public RelyingPartyRegistrationResolver relyingPartyRegistrationResolver()
{
- return new
StandardRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository(),
properties.getAllowedContextPathsAsList());
+ return new
StandardRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository());
}
/**
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolver.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolver.java
index 961b199a30..70af1f2652 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolver.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolver.java
@@ -29,7 +29,6 @@ import org.springframework.web.util.UriComponentsBuilder;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -45,17 +44,13 @@ public class StandardRelyingPartyRegistrationResolver
implements Converter<HttpS
private final RelyingPartyRegistrationRepository repository;
- private final List<String> allowedContextPaths;
-
/**
* Standard Resolver with Registration Repository and Allowed Context
Paths from application properties
*
* @param repository Relying Party Registration Repository required
- * @param allowedContextPaths Allowed Context Paths required
*/
- public StandardRelyingPartyRegistrationResolver(final
RelyingPartyRegistrationRepository repository, final List<String>
allowedContextPaths) {
+ public StandardRelyingPartyRegistrationResolver(final
RelyingPartyRegistrationRepository repository) {
this.repository = Objects.requireNonNull(repository, "Repository
required");
- this.allowedContextPaths = Objects.requireNonNull(allowedContextPaths,
"Allowed Context Paths required");
}
/**
@@ -116,7 +111,7 @@ public class StandardRelyingPartyRegistrationResolver
implements Converter<HttpS
}
private String getBaseUrl(final HttpServletRequest request) {
- final URI requestUri =
RequestUriBuilder.fromHttpServletRequest(request, allowedContextPaths).build();
+ final URI requestUri =
RequestUriBuilder.fromHttpServletRequest(request).build();
final String httpUrl = requestUri.toString();
final String contextPath = request.getContextPath();
return
UriComponentsBuilder.fromUriString(httpUrl).path(contextPath).replaceQuery(null).fragment(null).build().toString();
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolverTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolverTest.java
index d93dcccf7f..95e9f14da0 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolverTest.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/service/web/StandardRelyingPartyRegistrationResolverTest.java
@@ -27,8 +27,6 @@ import org.springframework.mock.web.MockHttpServletRequest;
import
org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import
org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
-import java.util.Collections;
-
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -57,6 +55,8 @@ class StandardRelyingPartyRegistrationResolverTest {
private static final String REGISTRATION_ID =
Saml2RegistrationProperty.REGISTRATION_ID.getProperty();
+ private static final String ALLOWED_CONTEXT_PATHS = "allowedContextPaths";
+
@Mock
RelyingPartyRegistrationRepository repository;
@@ -73,7 +73,7 @@ class StandardRelyingPartyRegistrationResolverTest {
@Test
void testResolveNotFound() {
- final StandardRelyingPartyRegistrationResolver resolver = new
StandardRelyingPartyRegistrationResolver(repository, Collections.emptyList());
+ final StandardRelyingPartyRegistrationResolver resolver = new
StandardRelyingPartyRegistrationResolver(repository);
final RelyingPartyRegistration registration =
resolver.resolve(request, REGISTRATION_ID);
@@ -82,7 +82,7 @@ class StandardRelyingPartyRegistrationResolverTest {
@Test
void testResolveFound() {
- final StandardRelyingPartyRegistrationResolver resolver = new
StandardRelyingPartyRegistrationResolver(repository, Collections.emptyList());
+ final StandardRelyingPartyRegistrationResolver resolver = new
StandardRelyingPartyRegistrationResolver(repository);
final RelyingPartyRegistration registration =
getRegistrationBuilder().build();
when(repository.findByRegistrationId(eq(REGISTRATION_ID))).thenReturn(registration);
@@ -95,7 +95,9 @@ class StandardRelyingPartyRegistrationResolverTest {
@Test
void testResolveSingleLogoutForwardedPathFound() {
- final StandardRelyingPartyRegistrationResolver resolver = new
StandardRelyingPartyRegistrationResolver(repository,
Collections.singletonList(FORWARDED_PATH));
+ request.getServletContext().setInitParameter(ALLOWED_CONTEXT_PATHS,
FORWARDED_PATH);
+
+ final StandardRelyingPartyRegistrationResolver resolver = new
StandardRelyingPartyRegistrationResolver(repository);
final RelyingPartyRegistration registration =
getSingleLogoutRegistration();
when(repository.findByRegistrationId(eq(REGISTRATION_ID))).thenReturn(registration);