mirror of https://github.com/apache/nifi.git
NIFI-9481 Excluded Data Transfer REST methods from DoSFilter
- Added DataTransferDoSFilter with request URI evaluation - Added RequestFilterProvider and implementations to abstract Jetty Filter configuration Signed-off-by: Joe Gresock <jgresock@gmail.com> This closes #5670.
This commit is contained in:
parent
70aa719cbf
commit
fc27b3138b
|
@ -54,13 +54,10 @@ import org.apache.nifi.util.NiFiProperties;
|
|||
import org.apache.nifi.web.ContentAccess;
|
||||
import org.apache.nifi.web.NiFiWebConfigurationContext;
|
||||
import org.apache.nifi.web.UiExtensionType;
|
||||
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
|
||||
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
|
||||
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
|
||||
import org.apache.nifi.web.security.requests.ContentLengthFilter;
|
||||
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
|
||||
import org.apache.nifi.web.server.filter.FilterParameter;
|
||||
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.apache.nifi.web.server.util.TrustStoreScanner;
|
||||
|
@ -83,7 +80,6 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.servlets.DoSFilter;
|
||||
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
|
@ -99,7 +95,6 @@ import org.springframework.web.context.WebApplicationContext;
|
|||
import org.springframework.web.context.support.WebApplicationContextUtils;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.ServletContext;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
|
@ -147,9 +142,9 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
|||
private static final String CONTEXT_PATH_NIFI_API = "/nifi-api";
|
||||
private static final String CONTEXT_PATH_NIFI_CONTENT_VIEWER = "/nifi-content-viewer";
|
||||
private static final String CONTEXT_PATH_NIFI_DOCS = "/nifi-docs";
|
||||
private static final String RELATIVE_PATH_ACCESS_TOKEN = "/access/token";
|
||||
|
||||
private static final int DOS_FILTER_REJECT_REQUEST = -1;
|
||||
private static final RequestFilterProvider REQUEST_FILTER_PROVIDER = new StandardRequestFilterProvider();
|
||||
private static final RequestFilterProvider REST_API_REQUEST_FILTER_PROVIDER = new RestApiRequestFilterProvider();
|
||||
|
||||
private static final FileFilter WAR_FILTER = pathname -> {
|
||||
final String nameToTest = pathname.getName().toLowerCase();
|
||||
|
@ -214,15 +209,11 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
|||
if (props.isHTTPSConfigured()) {
|
||||
// Create a handler for the host header and add it to the server
|
||||
final HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
|
||||
logger.info("Created HostHeaderHandler [{}}]", hostHeaderHandler);
|
||||
|
||||
// Add this before the WAR handlers
|
||||
allHandlers.addHandler(hostHeaderHandler);
|
||||
} else {
|
||||
logger.info("Running in HTTP mode; host headers not restricted");
|
||||
}
|
||||
|
||||
|
||||
final ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
|
||||
contextHandlers.addHandler(warHandlers);
|
||||
allHandlers.addHandler(contextHandlers);
|
||||
|
@ -239,14 +230,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
|||
server.setRequestLog(requestLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates this object but does not perform any configuration. Used for unit testing.
|
||||
*/
|
||||
JettyServer(Server server, NiFiProperties properties) {
|
||||
this.server = server;
|
||||
this.props = properties;
|
||||
}
|
||||
|
||||
private Handler loadInitialWars(final Set<Bundle> bundles) {
|
||||
|
||||
// load WARs
|
||||
|
@ -633,25 +616,15 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
|||
// configure the max form size (3x the default)
|
||||
webappContext.setMaxFormContentSize(600000);
|
||||
|
||||
// add HTTP security headers to all responses
|
||||
// TODO: Allow more granular path configuration (e.g. /nifi-api/site-to-site/ vs. /nifi-api/process-groups)
|
||||
ArrayList<Class<? extends Filter>> filters =
|
||||
new ArrayList<>(Arrays.asList(
|
||||
XFrameOptionsFilter.class,
|
||||
ContentSecurityPolicyFilter.class,
|
||||
XSSProtectionFilter.class,
|
||||
XContentTypeOptionsFilter.class));
|
||||
final List<FilterHolder> requestFilters = CONTEXT_PATH_NIFI_API.equals(contextPath)
|
||||
? REST_API_REQUEST_FILTER_PROVIDER.getFilters(props)
|
||||
: REQUEST_FILTER_PROVIDER.getFilters(props);
|
||||
|
||||
if (props.isHTTPSConfigured()) {
|
||||
filters.add(StrictTransportSecurityFilter.class);
|
||||
filters.add(RequestAuthenticationFilter.class);
|
||||
}
|
||||
filters.forEach((filter) -> addFilters(filter, webappContext));
|
||||
addDenialOfServiceFilters(webappContext, props);
|
||||
|
||||
if (CONTEXT_PATH_NIFI_API.equals(contextPath)) {
|
||||
addAccessTokenRequestFilter(webappContext, props);
|
||||
}
|
||||
requestFilters.forEach(filter -> {
|
||||
final String pathSpecification = filter.getInitParameter(FilterParameter.PATH_SPECIFICATION.name());
|
||||
final String filterPathSpecification = pathSpecification == null ? CONTEXT_PATH_ALL : pathSpecification;
|
||||
webappContext.addFilter(filter, filterPathSpecification, EnumSet.allOf(DispatcherType.class));
|
||||
});
|
||||
|
||||
try {
|
||||
// configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
|
||||
|
@ -660,16 +633,10 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
|||
startUpFailure(ioe);
|
||||
}
|
||||
|
||||
logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
|
||||
logger.info("Loading WAR [{}] Context Path [{}]", warFile.getAbsolutePath(), contextPath);
|
||||
return webappContext;
|
||||
}
|
||||
|
||||
private void addFilters(Class<? extends Filter> clazz, WebAppContext webappContext) {
|
||||
FilterHolder holder = new FilterHolder(clazz);
|
||||
holder.setName(clazz.getSimpleName());
|
||||
webappContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
|
||||
}
|
||||
|
||||
private void addDocsServlets(WebAppContext docsContext) {
|
||||
try {
|
||||
// Load the nifi/docs directory
|
||||
|
@ -712,108 +679,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds configurable filters relating to preventing denial of service attacks to the given context.
|
||||
* Currently, this implementation adds
|
||||
* {@link org.eclipse.jetty.servlets.DoSFilter} and {@link ContentLengthFilter} filters.
|
||||
*
|
||||
* @param webAppContext context to which filters will be added
|
||||
* @param props the {@link NiFiProperties}
|
||||
*/
|
||||
private static void addDenialOfServiceFilters(final WebAppContext webAppContext, final NiFiProperties props) {
|
||||
addWebRequestLimitingFilter(webAppContext, props.getMaxWebRequestsPerSecond(), getWebRequestTimeoutMs(props), props.getWebRequestIpWhitelist());
|
||||
|
||||
// Only add the ContentLengthFilter if the property is explicitly set (empty by default)
|
||||
final int maxRequestSize = determineMaxRequestSize(props);
|
||||
if (maxRequestSize > 0) {
|
||||
addContentLengthFilter(webAppContext, maxRequestSize);
|
||||
} else {
|
||||
logger.debug("Not adding content-length filter because {} is not set in nifi.properties", NiFiProperties.WEB_MAX_CONTENT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
private static long getWebRequestTimeoutMs(final NiFiProperties props) {
|
||||
final long defaultRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_WEB_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS));
|
||||
long configuredRequestTimeout = 0L;
|
||||
try {
|
||||
configuredRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(props.getWebRequestTimeout(), TimeUnit.MILLISECONDS));
|
||||
} catch (final NumberFormatException e) {
|
||||
logger.warn("Exception parsing property [{}]; using default value: [{}]", NiFiProperties.WEB_REQUEST_TIMEOUT, defaultRequestTimeout);
|
||||
}
|
||||
|
||||
return configuredRequestTimeout > 0 ? configuredRequestTimeout : defaultRequestTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@link org.eclipse.jetty.servlets.DoSFilter} to the specified context and path. Limits incoming web requests to {@code maxWebRequestsPerSecond} per second.
|
||||
* In order to allow clients to make more requests than the maximum rate, clients can be added to the {@code ipWhitelist}.
|
||||
* The {@code requestTimeoutInMilliseconds} value limits requests to the given request timeout amount, and will close connections that run longer than this time.
|
||||
*
|
||||
* @param webAppContext Web Application Context where Filter will be added
|
||||
* @param maxRequestsPerSec Maximum number of allowed requests per second
|
||||
* @param maxRequestMs Maximum amount of time in milliseconds before a connection will be automatically closed
|
||||
* @param allowed Comma-separated string of IP addresses that should not be rate limited. Does not apply to request timeout
|
||||
*/
|
||||
private static void addWebRequestLimitingFilter(final WebAppContext webAppContext, final int maxRequestsPerSec, final long maxRequestMs, final String allowed) {
|
||||
final FilterHolder holder = new FilterHolder(DoSFilter.class);
|
||||
holder.setInitParameters(new HashMap<String, String>() {{
|
||||
put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
|
||||
put("maxRequestMs", Long.toString(maxRequestMs));
|
||||
put("ipWhitelist", allowed);
|
||||
}});
|
||||
holder.setName(DoSFilter.class.getSimpleName());
|
||||
|
||||
webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
|
||||
logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", CONTEXT_PATH_ALL, maxRequestsPerSec, maxRequestMs, allowed);
|
||||
}
|
||||
|
||||
private static void addAccessTokenRequestFilter(final WebAppContext webAppContext, final NiFiProperties properties) {
|
||||
final int maxRequestsPerSec = properties.getMaxWebAccessTokenRequestsPerSecond();
|
||||
final long maxRequestMs = getWebRequestTimeoutMs(properties);
|
||||
|
||||
final String webRequestAllowed = properties.getWebRequestIpWhitelist();
|
||||
final FilterHolder holder = new FilterHolder(DoSFilter.class);
|
||||
holder.setInitParameters(new HashMap<String, String>() {{
|
||||
put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
|
||||
put("maxRequestMs", Long.toString(maxRequestMs));
|
||||
put("ipWhitelist", webRequestAllowed);
|
||||
put("maxWaitMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
|
||||
put("delayMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
|
||||
}});
|
||||
holder.setName("AccessTokenRequest-DoSFilter");
|
||||
|
||||
webAppContext.addFilter(holder, RELATIVE_PATH_ACCESS_TOKEN, EnumSet.allOf(DispatcherType.class));
|
||||
logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", RELATIVE_PATH_ACCESS_TOKEN, maxRequestsPerSec, maxRequestMs, webRequestAllowed);
|
||||
}
|
||||
|
||||
private static int determineMaxRequestSize(NiFiProperties props) {
|
||||
try {
|
||||
final String webMaxContentSize = props.getWebMaxContentSize();
|
||||
logger.debug("Read {} as {}", NiFiProperties.WEB_MAX_CONTENT_SIZE, webMaxContentSize);
|
||||
if (StringUtils.isNotBlank(webMaxContentSize)) {
|
||||
int configuredMaxRequestSize = DataUnit.parseDataSize(webMaxContentSize, DataUnit.B).intValue();
|
||||
logger.debug("Parsed max content length as {} bytes", configuredMaxRequestSize);
|
||||
return configuredMaxRequestSize;
|
||||
} else {
|
||||
logger.debug("{} read from nifi.properties is empty", NiFiProperties.WEB_MAX_CONTENT_SIZE);
|
||||
}
|
||||
} catch (final IllegalArgumentException e) {
|
||||
logger.warn("Exception parsing property {}; disabling content length filter", NiFiProperties.WEB_MAX_CONTENT_SIZE);
|
||||
logger.debug("Error during parsing: ", e);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void addContentLengthFilter(final WebAppContext webAppContext, int maxContentLength) {
|
||||
final FilterHolder holder = new FilterHolder(ContentLengthFilter.class);
|
||||
holder.setInitParameters(new HashMap<String, String>() {{
|
||||
put("maxContentLength", String.valueOf(maxContentLength));
|
||||
}});
|
||||
holder.setName(ContentLengthFilter.class.getSimpleName());
|
||||
logger.debug("Adding ContentLengthFilter to Path [{}] with Maximum Content Length [{}B]", CONTEXT_PATH_ALL, maxContentLength);
|
||||
webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a File object for the directory containing NIFI documentation.
|
||||
* <p>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.eclipse.jetty.servlets.DoSFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Denial-of-Service Filter extended to exclude Data Transfer operations
|
||||
*/
|
||||
public class DataTransferExcludedDoSFilter extends DoSFilter {
|
||||
protected static final String DATA_TRANSFER_URI_ATTRIBUTE = "nifi-api-data-transfer-uri";
|
||||
|
||||
private static final String DATA_TRANSFER_PATH = "/nifi-api/data-transfer";
|
||||
|
||||
/**
|
||||
* Handle Filter Chain and override service filter for Data Transfer requests
|
||||
*
|
||||
* @param filterChain Filter Chain
|
||||
* @param request HTTP Servlet Request to be evaluated
|
||||
* @param response HTTP Servlet Response
|
||||
* @throws ServletException Thrown on FilterChain.doFilter() failures
|
||||
* @throws IOException Thrown on FilterChain.doFilter() failures
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterChain(final FilterChain filterChain, final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
|
||||
final String requestUri = request.getRequestURI();
|
||||
if (requestUri.startsWith(DATA_TRANSFER_PATH)) {
|
||||
request.setAttribute(DATA_TRANSFER_URI_ATTRIBUTE, requestUri);
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
super.doFilterChain(filterChain, request, response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
/**
|
||||
* Filter Parameter enumeration
|
||||
*/
|
||||
public enum FilterParameter {
|
||||
PATH_SPECIFICATION
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Request Filter Provider for abstracting configuration of HTTP Request Filters
|
||||
*/
|
||||
public interface RequestFilterProvider {
|
||||
/**
|
||||
* Get Filters using provided NiFi Properties
|
||||
*
|
||||
* @param properties NiFi Properties required
|
||||
* @return List of Filter Holder
|
||||
*/
|
||||
List<FilterHolder> getFilters(NiFiProperties properties);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlets.DoSFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Request Filter Provider for REST API Web Application
|
||||
*/
|
||||
public class RestApiRequestFilterProvider extends StandardRequestFilterProvider {
|
||||
public static final String RELATIVE_PATH_ACCESS_TOKEN = "/access/token";
|
||||
|
||||
private static final int DOS_FILTER_REJECT_REQUEST = -1;
|
||||
|
||||
/**
|
||||
* Get Filters using provided NiFi Properties and append filters for Access Token Requests
|
||||
*
|
||||
* @param properties NiFi Properties required
|
||||
* @return List of Filter Holders
|
||||
*/
|
||||
@Override
|
||||
public List<FilterHolder> getFilters(final NiFiProperties properties) {
|
||||
final List<FilterHolder> filters = super.getFilters(properties);
|
||||
|
||||
final FilterHolder accessTokenDenialOfServiceFilter = getAccessTokenDenialOfServiceFilter(properties);
|
||||
filters.add(accessTokenDenialOfServiceFilter);
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
private FilterHolder getAccessTokenDenialOfServiceFilter(final NiFiProperties properties) {
|
||||
final FilterHolder filter = getDenialOfServiceFilter(properties, DoSFilter.class);
|
||||
|
||||
final int maxWebAccessTokenRequestsPerSecond = properties.getMaxWebAccessTokenRequestsPerSecond();
|
||||
filter.setInitParameter("maxRequestsPerSec", Integer.toString(maxWebAccessTokenRequestsPerSecond));
|
||||
|
||||
filter.setInitParameter("maxWaitMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
|
||||
filter.setInitParameter("delayMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
|
||||
|
||||
filter.setInitParameter(FilterParameter.PATH_SPECIFICATION.name(), RELATIVE_PATH_ACCESS_TOKEN);
|
||||
filter.setName("AccessToken-DoSFilter");
|
||||
return filter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.processor.DataUnit;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
|
||||
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
|
||||
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
|
||||
import org.apache.nifi.web.security.requests.ContentLengthFilter;
|
||||
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlets.DoSFilter;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Standard implementation of Request Filter Provider
|
||||
*/
|
||||
public class StandardRequestFilterProvider implements RequestFilterProvider {
|
||||
private static final int MAX_CONTENT_SIZE_DISABLED = 0;
|
||||
|
||||
/**
|
||||
* Get Filters using provided NiFi Properties
|
||||
*
|
||||
* @param properties NiFi Properties required
|
||||
* @return List of Filter Holders
|
||||
*/
|
||||
@Override
|
||||
public List<FilterHolder> getFilters(final NiFiProperties properties) {
|
||||
Objects.requireNonNull(properties, "Properties required");
|
||||
|
||||
final List<FilterHolder> filters = new ArrayList<>();
|
||||
|
||||
filters.add(getFilterHolder(XFrameOptionsFilter.class));
|
||||
filters.add(getFilterHolder(ContentSecurityPolicyFilter.class));
|
||||
filters.add(getFilterHolder(XSSProtectionFilter.class));
|
||||
filters.add(getFilterHolder(XContentTypeOptionsFilter.class));
|
||||
|
||||
if (properties.isHTTPSConfigured()) {
|
||||
filters.add(getFilterHolder(StrictTransportSecurityFilter.class));
|
||||
filters.add(getFilterHolder(RequestAuthenticationFilter.class));
|
||||
}
|
||||
|
||||
final int maxContentSize = getMaxContentSize(properties);
|
||||
if (maxContentSize > MAX_CONTENT_SIZE_DISABLED) {
|
||||
final FilterHolder contentLengthFilter = getContentLengthFilter(maxContentSize);
|
||||
filters.add(contentLengthFilter);
|
||||
}
|
||||
|
||||
final FilterHolder denialOfServiceFilter = getDenialOfServiceFilter(properties, DataTransferExcludedDoSFilter.class);
|
||||
filters.add(denialOfServiceFilter);
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
protected FilterHolder getDenialOfServiceFilter(final NiFiProperties properties, final Class<? extends DoSFilter> filterClass) {
|
||||
final FilterHolder filter = new FilterHolder(filterClass);
|
||||
|
||||
final int maxWebRequestsPerSecond = properties.getMaxWebRequestsPerSecond();
|
||||
filter.setInitParameter("maxRequestsPerSec", Integer.toString(maxWebRequestsPerSecond));
|
||||
|
||||
final long webRequestTimeout = getWebRequestTimeout(properties);
|
||||
filter.setInitParameter("maxRequestMs", Long.toString(webRequestTimeout));
|
||||
|
||||
final String webRequestIpWhitelist = properties.getWebRequestIpWhitelist();
|
||||
filter.setInitParameter("ipWhitelist", webRequestIpWhitelist);
|
||||
|
||||
filter.setName(DoSFilter.class.getSimpleName());
|
||||
return filter;
|
||||
}
|
||||
|
||||
private FilterHolder getFilterHolder(final Class<? extends Filter> filterClass) {
|
||||
final FilterHolder filter = new FilterHolder(filterClass);
|
||||
filter.setName(filterClass.getSimpleName());
|
||||
return filter;
|
||||
}
|
||||
|
||||
private FilterHolder getContentLengthFilter(final int maxContentSize) {
|
||||
final FilterHolder filter = new FilterHolder(ContentLengthFilter.class);
|
||||
filter.setInitParameter(ContentLengthFilter.MAX_LENGTH_INIT_PARAM, Integer.toString(maxContentSize));
|
||||
filter.setName(ContentLengthFilter.class.getSimpleName());
|
||||
return filter;
|
||||
}
|
||||
|
||||
private int getMaxContentSize(final NiFiProperties properties) {
|
||||
final String webMaxContentSize = properties.getWebMaxContentSize();
|
||||
try {
|
||||
return StringUtils.isBlank(webMaxContentSize) ? MAX_CONTENT_SIZE_DISABLED : DataUnit.parseDataSize(webMaxContentSize, DataUnit.B).intValue();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new IllegalStateException(String.format("Property [%s] format invalid", NiFiProperties.WEB_MAX_CONTENT_SIZE), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected long getWebRequestTimeout(final NiFiProperties properties) {
|
||||
final String webRequestTimeout = properties.getWebRequestTimeout();
|
||||
|
||||
try {
|
||||
final double webRequestTimeoutParsed = FormatUtils.getPreciseTimeDuration(webRequestTimeout, TimeUnit.MILLISECONDS);
|
||||
return Math.round(webRequestTimeoutParsed);
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new IllegalStateException(String.format("Property [%s] format invalid", NiFiProperties.WEB_REQUEST_TIMEOUT), e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,435 +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.nifi.bundle.Bundle
|
||||
import org.apache.nifi.nar.ExtensionManagerHolder
|
||||
import org.apache.nifi.processor.DataUnit
|
||||
import org.apache.nifi.remote.io.socket.NetworkUtils
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration
|
||||
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder
|
||||
import org.apache.nifi.security.util.TlsConfiguration
|
||||
import org.apache.nifi.security.util.TlsPlatform
|
||||
import org.apache.nifi.util.NiFiProperties
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.eclipse.jetty.server.Connector
|
||||
import org.eclipse.jetty.server.HttpConfiguration
|
||||
import org.eclipse.jetty.server.Server
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.eclipse.jetty.server.SslConnectionFactory
|
||||
import org.eclipse.jetty.servlet.FilterHolder
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory
|
||||
import org.eclipse.jetty.webapp.WebAppContext
|
||||
import org.junit.After
|
||||
import org.junit.Assume
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.contrib.java.lang.system.Assertion
|
||||
import org.junit.contrib.java.lang.system.ExpectedSystemExit
|
||||
import org.junit.contrib.java.lang.system.SystemErrRule
|
||||
import org.junit.contrib.java.lang.system.SystemOutRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.invocation.InvocationOnMock
|
||||
import org.mockito.stubbing.Answer
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.servlet.DispatcherType
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.Security
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
class JettyServerGroovyTest extends GroovyTestCase {
|
||||
private static final Logger logger = LoggerFactory.getLogger(JettyServerGroovyTest.class)
|
||||
|
||||
@Rule
|
||||
public final ExpectedSystemExit exit = ExpectedSystemExit.none()
|
||||
|
||||
@Rule
|
||||
public final SystemOutRule systemOutRule = new SystemOutRule().enableLog()
|
||||
|
||||
@Rule
|
||||
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog()
|
||||
|
||||
private static final int DEFAULT_HTTP_PORT = 8080
|
||||
private static final int DEFAULT_HTTPS_PORT = 8443
|
||||
|
||||
private static final int HTTPS_PORT = NetworkUtils.getAvailableTcpPort()
|
||||
private static final String HTTPS_HOSTNAME = "localhost"
|
||||
|
||||
private static final String TLS_1_3_PROTOCOL = "TLSv1.3"
|
||||
private static final List<String> TLS_1_3_CIPHER_SUITES = ["TLS_AES_128_GCM_SHA256"]
|
||||
|
||||
private static final TlsConfiguration TLS_CONFIGURATION = new TemporaryKeyStoreBuilder().build()
|
||||
|
||||
// These protocol versions should not ever be supported
|
||||
static private final List<String> LEGACY_TLS_PROTOCOLS = ["TLS", "TLSv1", "TLSv1.1", "SSL", "SSLv2", "SSLv2Hello", "SSLv3"]
|
||||
|
||||
NiFiProperties httpsProps = new NiFiProperties(new Properties([
|
||||
(NiFiProperties.WEB_HTTPS_PORT) : HTTPS_PORT as String,
|
||||
(NiFiProperties.WEB_HTTPS_HOST) : HTTPS_HOSTNAME,
|
||||
(NiFiProperties.SECURITY_KEYSTORE) : TLS_CONFIGURATION.keystorePath,
|
||||
(NiFiProperties.SECURITY_KEYSTORE_PASSWD) : TLS_CONFIGURATION.keystorePassword,
|
||||
(NiFiProperties.SECURITY_KEYSTORE_TYPE) : TLS_CONFIGURATION.keystoreType.type,
|
||||
(NiFiProperties.SECURITY_TRUSTSTORE) : TLS_CONFIGURATION.truststorePath,
|
||||
(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD): TLS_CONFIGURATION.truststorePassword,
|
||||
(NiFiProperties.SECURITY_TRUSTSTORE_TYPE) : TLS_CONFIGURATION.truststoreType.type,
|
||||
]))
|
||||
|
||||
@BeforeClass
|
||||
static void setUpOnce() throws Exception {
|
||||
new File(TLS_CONFIGURATION.keystorePath).deleteOnExit()
|
||||
new File(TLS_CONFIGURATION.truststorePath).deleteOnExit()
|
||||
|
||||
Security.addProvider(new BouncyCastleProvider())
|
||||
|
||||
logger.metaClass.methodMissing = { String name, args ->
|
||||
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
void tearDown() throws Exception {
|
||||
// Cleans up the EMH so it can be reinitialized when a new Jetty server starts
|
||||
ExtensionManagerHolder.INSTANCE = null
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldDetectHttpAndHttpsConfigurationsBothPresent() {
|
||||
// Arrange
|
||||
Map badProps = [
|
||||
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
|
||||
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
|
||||
(NiFiProperties.WEB_THREADS) : NiFiProperties.DEFAULT_WEB_THREADS
|
||||
]
|
||||
NiFiProperties mockProps = Mockito.mock(NiFiProperties.class)
|
||||
Mockito.when(mockProps.getPort()).thenReturn(DEFAULT_HTTP_PORT)
|
||||
Mockito.when(mockProps.getSslPort()).thenReturn(DEFAULT_HTTPS_PORT)
|
||||
|
||||
Mockito.when(mockProps.getProperty(ArgumentMatchers.anyString())).thenAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
badProps[(String) invocation.getArgument(0)] ?: "no_value"
|
||||
}
|
||||
})
|
||||
|
||||
// Act
|
||||
boolean bothConfigsPresent = JettyServer.bothHttpAndHttpsConnectorsConfigured(mockProps)
|
||||
logger.info("Both configs present: ${bothConfigsPresent}")
|
||||
|
||||
// Assert
|
||||
assert bothConfigsPresent
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetectHttpAndHttpsConfigurationsShouldAllowEither() {
|
||||
// Arrange
|
||||
Map httpMap = [
|
||||
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
|
||||
(NiFiProperties.WEB_HTTPS_HOST): null,
|
||||
]
|
||||
NiFiProperties httpProps = [
|
||||
getPort : { -> DEFAULT_HTTP_PORT },
|
||||
getSslPort : { -> null },
|
||||
getProperty: { String prop ->
|
||||
String value = httpMap[prop] ?: "no_value"
|
||||
value
|
||||
},
|
||||
] as NiFiProperties
|
||||
|
||||
Map httpsMap = [
|
||||
(NiFiProperties.WEB_HTTP_HOST) : null,
|
||||
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
|
||||
]
|
||||
NiFiProperties httpsProps = [
|
||||
getPort : { -> null },
|
||||
getSslPort : { -> DEFAULT_HTTPS_PORT },
|
||||
getProperty: { String prop ->
|
||||
String value = httpsMap[prop] ?: "no_value"
|
||||
value
|
||||
},
|
||||
] as NiFiProperties
|
||||
|
||||
// Act
|
||||
boolean bothConfigsPresentForHttp = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpProps)
|
||||
logger.info("Both configs present for HTTP properties: ${bothConfigsPresentForHttp}")
|
||||
|
||||
boolean bothConfigsPresentForHttps = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpsProps)
|
||||
logger.info("Both configs present for HTTPS properties: ${bothConfigsPresentForHttps}")
|
||||
|
||||
// Assert
|
||||
assert !bothConfigsPresentForHttp
|
||||
assert !bothConfigsPresentForHttps
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldFailToStartWithHttpAndHttpsConfigurationsBothPresent() {
|
||||
// Arrange
|
||||
Map badProps = [
|
||||
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
|
||||
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
|
||||
]
|
||||
NiFiProperties mockProps = [
|
||||
getPort : { -> DEFAULT_HTTP_PORT },
|
||||
getSslPort : { -> DEFAULT_HTTPS_PORT },
|
||||
getProperty : { String prop ->
|
||||
String value = badProps[prop] ?: "no_value"
|
||||
logger.mock("getProperty(${prop}) -> ${value}")
|
||||
value
|
||||
},
|
||||
getWebThreads : { -> NiFiProperties.DEFAULT_WEB_THREADS },
|
||||
getWebMaxHeaderSize: { -> NiFiProperties.DEFAULT_WEB_MAX_HEADER_SIZE },
|
||||
isHTTPSConfigured : { -> true }
|
||||
] as NiFiProperties
|
||||
|
||||
// The web server should fail to start and exit Java
|
||||
exit.expectSystemExitWithStatus(1)
|
||||
exit.checkAssertionAfterwards(new Assertion() {
|
||||
void checkAssertion() {
|
||||
final String standardErr = systemErrRule.getLog()
|
||||
List<String> errLines = standardErr.split("\n")
|
||||
|
||||
assert errLines.any { it =~ "Failed to start web server: " }
|
||||
assert errLines.any { it =~ "Shutting down..." }
|
||||
}
|
||||
})
|
||||
|
||||
// Act
|
||||
JettyServer jettyServer = new JettyServer()
|
||||
jettyServer.initialize(mockProps, null, [] as Set<Bundle>, null)
|
||||
|
||||
// Assert
|
||||
|
||||
// Assertions defined above
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldConfigureHTTPSConnector() {
|
||||
// Arrange
|
||||
final String externalHostname = "localhost"
|
||||
|
||||
NiFiProperties httpsProps = new NiFiProperties(new Properties([
|
||||
(NiFiProperties.WEB_HTTPS_PORT): HTTPS_PORT as String,
|
||||
(NiFiProperties.WEB_HTTPS_HOST): externalHostname,
|
||||
]))
|
||||
|
||||
Server internalServer = new Server()
|
||||
JettyServer jetty = new JettyServer(internalServer, httpsProps)
|
||||
|
||||
// Act
|
||||
jetty.configureHttpsConnector(internalServer, new HttpConfiguration())
|
||||
List<Connector> connectors = Arrays.asList(internalServer.connectors)
|
||||
|
||||
// Assert
|
||||
assertServerConnector(connectors, externalHostname, HTTPS_PORT)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldSupportTLSv1_3WhenProtocolFound() {
|
||||
// Arrange
|
||||
String[] defaultProtocols = SSLContext.getDefault().defaultSSLParameters.protocols
|
||||
Assume.assumeTrue("This test should only run when TLSv1.3 is found in the set of default protocols", defaultProtocols.contains(TLS_1_3_PROTOCOL))
|
||||
|
||||
Server internalServer = new Server()
|
||||
JettyServer jetty = new JettyServer(internalServer, httpsProps)
|
||||
|
||||
jetty.configureConnectors(internalServer)
|
||||
List<Connector> connectors = Arrays.asList(internalServer.connectors)
|
||||
internalServer.start()
|
||||
|
||||
// Create a (client) socket which only supports TLSv1.3
|
||||
TlsConfiguration tls13ClientConf = StandardTlsConfiguration.fromNiFiProperties(httpsProps)
|
||||
SSLSocketFactory socketFactory = org.apache.nifi.security.util.SslContextFactory.createSSLSocketFactory(tls13ClientConf)
|
||||
|
||||
SSLSocket socket = (SSLSocket) socketFactory.createSocket(HTTPS_HOSTNAME, HTTPS_PORT)
|
||||
socket.setEnabledProtocols([TLS_1_3_PROTOCOL] as String[])
|
||||
socket.setEnabledCipherSuites(TLS_1_3_CIPHER_SUITES as String[])
|
||||
|
||||
// Act
|
||||
String response = makeTLSRequest(socket, "This is a TLS 1.3 request")
|
||||
|
||||
// Assert
|
||||
assert response =~ "HTTP/1.1 400"
|
||||
|
||||
assertServerConnector(connectors)
|
||||
|
||||
// Clean up
|
||||
internalServer.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldNotSupportTLSv1_3WhenProtocolNotFound() {
|
||||
// Arrange
|
||||
Assume.assumeTrue("This test should only run when TLSv1.3 is not found in the set of default protocols", !TlsPlatform.supportedProtocols.contains(TLS_1_3_PROTOCOL))
|
||||
|
||||
Server internalServer = new Server()
|
||||
JettyServer jetty = new JettyServer(internalServer, httpsProps)
|
||||
|
||||
jetty.configureConnectors(internalServer)
|
||||
List<Connector> connectors = Arrays.asList(internalServer.connectors)
|
||||
internalServer.start()
|
||||
|
||||
TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(httpsProps)
|
||||
|
||||
// Create a "default" (client) socket (which supports TLSv1.2)
|
||||
SSLSocketFactory defaultSocketFactory = org.apache.nifi.security.util.SslContextFactory.createSSLSocketFactory(tlsConfiguration)
|
||||
SSLSocket defaultSocket = (SSLSocket) defaultSocketFactory.createSocket(HTTPS_HOSTNAME, HTTPS_PORT)
|
||||
|
||||
// Act
|
||||
String tls12Response = makeTLSRequest(defaultSocket, "This is a default socket request")
|
||||
|
||||
def msg = shouldFail() {
|
||||
// Create a (client) socket which only supports TLSv1.3
|
||||
SSLSocketFactory tls13SocketFactory = org.apache.nifi.security.util.SslContextFactory.createSSLSocketFactory(tlsConfiguration)
|
||||
|
||||
SSLSocket tls13Socket = (SSLSocket) tls13SocketFactory.createSocket(HTTPS_HOSTNAME, HTTPS_PORT)
|
||||
tls13Socket.setEnabledProtocols([TLS_1_3_PROTOCOL] as String[])
|
||||
tls13Socket.setEnabledCipherSuites(TLS_1_3_CIPHER_SUITES as String[])
|
||||
|
||||
makeTLSRequest(tls13Socket, "This is a TLSv1.3 socket request")
|
||||
}
|
||||
logger.expected(msg)
|
||||
|
||||
// Assert
|
||||
assert tls12Response =~ "HTTP"
|
||||
|
||||
assertServerConnector(connectors)
|
||||
|
||||
// Clean up
|
||||
internalServer.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the server's response body as a String. Closes the socket connection.
|
||||
*
|
||||
* @param socket
|
||||
* @param requestMessage
|
||||
* @return
|
||||
*/
|
||||
private static String makeTLSRequest(Socket socket, String requestMessage) {
|
||||
InputStream socketInputStream = new BufferedInputStream(socket.getInputStream())
|
||||
OutputStream socketOutputStream = new BufferedOutputStream(socket.getOutputStream())
|
||||
|
||||
socketOutputStream.write(requestMessage.getBytes())
|
||||
socketOutputStream.flush()
|
||||
|
||||
byte[] data = new byte[2048]
|
||||
int len = socketInputStream.read(data)
|
||||
if (len <= 0) {
|
||||
throw new IOException("no data received")
|
||||
}
|
||||
final String trimmedResponse = new String(data, 0, len, StandardCharsets.UTF_8)
|
||||
logger.info("Client received ${len} bytes from server: \n${trimmedResponse}\n----End of response----")
|
||||
socket.close()
|
||||
trimmedResponse
|
||||
}
|
||||
|
||||
private static void assertServerConnector(List<Connector> connectors,
|
||||
String EXPECTED_HOSTNAME = HTTPS_HOSTNAME,
|
||||
int EXPECTED_PORT = HTTPS_PORT) {
|
||||
// Assert the server connector is correct
|
||||
assert connectors.size() == 1
|
||||
ServerConnector connector = connectors.first() as ServerConnector
|
||||
assert connector.host == EXPECTED_HOSTNAME
|
||||
assert connector.port == EXPECTED_PORT
|
||||
assert connector.getProtocols() == ['ssl', 'http/1.1']
|
||||
|
||||
SslConnectionFactory connectionFactory = connector.getConnectionFactory("ssl") as SslConnectionFactory
|
||||
SslContextFactory sslContextFactory = connectionFactory.getSslContextFactory()
|
||||
assert (sslContextFactory.getExcludeProtocols() as List<String>).containsAll(LEGACY_TLS_PROTOCOLS)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldEnableContentLengthFilterIfWebMaxContentSizeSet() {
|
||||
// Arrange
|
||||
Map defaultProps = [
|
||||
(NiFiProperties.WEB_HTTP_PORT) : DEFAULT_HTTP_PORT as String,
|
||||
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
|
||||
(NiFiProperties.WEB_MAX_CONTENT_SIZE): "1 MB",
|
||||
]
|
||||
NiFiProperties mockProps = new NiFiProperties(new Properties(defaultProps))
|
||||
|
||||
List<FilterHolder> filters = []
|
||||
def mockWebContext = [
|
||||
addFilter: { FilterHolder fh, String path, EnumSet<DispatcherType> d ->
|
||||
logger.mock("Called addFilter(${fh.name}, ${path}, ${d})")
|
||||
filters.add(fh)
|
||||
fh
|
||||
}] as WebAppContext
|
||||
|
||||
JettyServer jettyServer = new JettyServer(new Server(), mockProps)
|
||||
logger.info("Created JettyServer: ${jettyServer.dump()}")
|
||||
|
||||
final int MAX_CONTENT_LENGTH_BYTES = DataUnit.parseDataSize(defaultProps[NiFiProperties.WEB_MAX_CONTENT_SIZE], DataUnit.B).intValue()
|
||||
|
||||
// Act
|
||||
jettyServer.addDenialOfServiceFilters(mockWebContext, mockProps)
|
||||
|
||||
// Assert
|
||||
assert filters.size() == 2
|
||||
def filterNames = filters*.name
|
||||
logger.info("Web API Context has ${filters.size()} filters: ${filterNames.join(", ")}".toString())
|
||||
assert filterNames.contains("DoSFilter")
|
||||
assert filterNames.contains("ContentLengthFilter")
|
||||
|
||||
FilterHolder clfHolder = filters.find { it.name == "ContentLengthFilter" }
|
||||
String maxContentLength = clfHolder.getInitParameter("maxContentLength")
|
||||
assert maxContentLength == MAX_CONTENT_LENGTH_BYTES as String
|
||||
|
||||
// Filter is not instantiated just by adding it
|
||||
// ContentLengthFilter clf = filters?.find { it.className == "ContentLengthFilter" }?.filter as ContentLengthFilter
|
||||
// assert clf.getMaxContentLength() == MAX_CONTENT_LENGTH_BYTES
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldNotEnableContentLengthFilterIfWebMaxContentSizeEmpty() {
|
||||
// Arrange
|
||||
Map defaultProps = [
|
||||
(NiFiProperties.WEB_HTTP_PORT): DEFAULT_HTTP_PORT as String,
|
||||
(NiFiProperties.WEB_HTTP_HOST): "localhost",
|
||||
]
|
||||
NiFiProperties mockProps = new NiFiProperties(new Properties(defaultProps))
|
||||
|
||||
List<FilterHolder> filters = []
|
||||
def mockWebContext = [
|
||||
addFilter: { FilterHolder fh, String path, EnumSet<DispatcherType> d ->
|
||||
logger.mock("Called addFilter(${fh.name}, ${path}, ${d})")
|
||||
filters.add(fh)
|
||||
fh
|
||||
}] as WebAppContext
|
||||
|
||||
JettyServer jettyServer = new JettyServer(new Server(), mockProps)
|
||||
logger.info("Created JettyServer: ${jettyServer.dump()}")
|
||||
|
||||
// Act
|
||||
jettyServer.addDenialOfServiceFilters(mockWebContext, mockProps)
|
||||
|
||||
// Assert
|
||||
assert filters.size() == 1
|
||||
def filterNames = filters*.name
|
||||
logger.info("Web API Context has ${filters.size()} filters: ${filterNames.join(", ")}".toString())
|
||||
assert filterNames.contains("DoSFilter")
|
||||
assert !filterNames.contains("ContentLengthFilter")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DataTransferExcludedDoSFilterTest {
|
||||
private static final String DATA_TRANSFER_URI = "/nifi-api/data-transfer";
|
||||
|
||||
private static final String ACCESS_URI = "/nifi-api/access";
|
||||
|
||||
@Mock
|
||||
private FilterConfig filterConfig;
|
||||
|
||||
@Mock
|
||||
private FilterChain filterChain;
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Mock
|
||||
private HttpServletResponse response;
|
||||
|
||||
private DataTransferExcludedDoSFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
public void setFilter() throws ServletException {
|
||||
filter = new DataTransferExcludedDoSFilter();
|
||||
filter.init(filterConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoFilterChain() throws ServletException, IOException {
|
||||
when(request.getRequestURI()).thenReturn(ACCESS_URI);
|
||||
|
||||
filter.doFilterChain(filterChain, request, response);
|
||||
|
||||
verify(request, never()).setAttribute(eq(DataTransferExcludedDoSFilter.DATA_TRANSFER_URI_ATTRIBUTE), eq(DATA_TRANSFER_URI));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoFilterChainDataTransfer() throws ServletException, IOException {
|
||||
when(request.getRequestURI()).thenReturn(DATA_TRANSFER_URI);
|
||||
|
||||
filter.doFilterChain(filterChain, request, response);
|
||||
|
||||
verify(request).setAttribute(eq(DataTransferExcludedDoSFilter.DATA_TRANSFER_URI_ATTRIBUTE), eq(DATA_TRANSFER_URI));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
|
||||
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlets.DoSFilter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class RestApiRequestFilterProviderTest {
|
||||
private RestApiRequestFilterProvider provider;
|
||||
|
||||
@BeforeEach
|
||||
public void setProvider() {
|
||||
provider = new RestApiRequestFilterProvider();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilters() {
|
||||
final NiFiProperties properties = getProperties(Collections.emptyMap());
|
||||
final List<FilterHolder> filters = provider.getFilters(properties);
|
||||
|
||||
final Optional<FilterHolder> filterHolder = filters.stream().filter(filter -> filter.getInitParameter(FilterParameter.PATH_SPECIFICATION.name()) != null).findFirst();
|
||||
assertTrue(filterHolder.isPresent());
|
||||
|
||||
final FilterHolder holder = filterHolder.get();
|
||||
assertEquals(DoSFilter.class, holder.getHeldClass());
|
||||
|
||||
assertNotNull(filters);
|
||||
assertFalse(filters.isEmpty());
|
||||
|
||||
assertFilterClassFound(filters, DataTransferExcludedDoSFilter.class);
|
||||
assertFilterClassFound(filters, XFrameOptionsFilter.class);
|
||||
assertFilterClassFound(filters, ContentSecurityPolicyFilter.class);
|
||||
assertFilterClassFound(filters, XSSProtectionFilter.class);
|
||||
assertFilterClassFound(filters, XContentTypeOptionsFilter.class);
|
||||
}
|
||||
|
||||
private void assertFilterClassFound(final List<FilterHolder> filters, final Class<? extends Filter> filterClass) {
|
||||
final Optional<FilterHolder> filterHolder = filters.stream().filter(filter -> filterClass.equals(filter.getHeldClass())).findFirst();
|
||||
assertTrue(filterHolder.isPresent(), String.format("Filter Class [%s] not found", filterClass));
|
||||
}
|
||||
|
||||
private NiFiProperties getProperties(final Map<String, String> properties) {
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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.filter;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
|
||||
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
|
||||
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
|
||||
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
|
||||
import org.apache.nifi.web.security.requests.ContentLengthFilter;
|
||||
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class StandardRequestFilterProviderTest {
|
||||
private static final String HTTPS_PORT = "8443";
|
||||
|
||||
private static final String MAX_CONTENT_SIZE = "1 MB";
|
||||
|
||||
private StandardRequestFilterProvider provider;
|
||||
|
||||
@BeforeEach
|
||||
public void setProvider() {
|
||||
provider = new StandardRequestFilterProvider();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilters() {
|
||||
final NiFiProperties properties = getProperties(Collections.emptyMap());
|
||||
final List<FilterHolder> filters = provider.getFilters(properties);
|
||||
|
||||
assertStandardFiltersFound(filters);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFiltersContentLengthEnabled() {
|
||||
final Map<String, String> configurationProperties = new LinkedHashMap<>();
|
||||
configurationProperties.put(NiFiProperties.WEB_MAX_CONTENT_SIZE, MAX_CONTENT_SIZE);
|
||||
|
||||
final NiFiProperties properties = getProperties(configurationProperties);
|
||||
final List<FilterHolder> filters = provider.getFilters(properties);
|
||||
|
||||
assertStandardFiltersFound(filters);
|
||||
|
||||
assertFilterClassFound(filters, ContentLengthFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFiltersHttpsEnabled() {
|
||||
final Map<String, String> configurationProperties = new LinkedHashMap<>();
|
||||
configurationProperties.put(NiFiProperties.WEB_HTTPS_PORT, HTTPS_PORT);
|
||||
|
||||
final NiFiProperties properties = getProperties(configurationProperties);
|
||||
final List<FilterHolder> filters = provider.getFilters(properties);
|
||||
|
||||
assertStandardFiltersFound(filters);
|
||||
|
||||
assertFilterClassFound(filters, RequestAuthenticationFilter.class);
|
||||
assertFilterClassFound(filters, StrictTransportSecurityFilter.class);
|
||||
}
|
||||
|
||||
private void assertStandardFiltersFound(final List<FilterHolder> filters) {
|
||||
assertNotNull(filters);
|
||||
assertFalse(filters.isEmpty());
|
||||
|
||||
assertFilterClassFound(filters, DataTransferExcludedDoSFilter.class);
|
||||
assertFilterClassFound(filters, XFrameOptionsFilter.class);
|
||||
assertFilterClassFound(filters, ContentSecurityPolicyFilter.class);
|
||||
assertFilterClassFound(filters, XSSProtectionFilter.class);
|
||||
assertFilterClassFound(filters, XContentTypeOptionsFilter.class);
|
||||
}
|
||||
|
||||
private void assertFilterClassFound(final List<FilterHolder> filters, final Class<? extends Filter> filterClass) {
|
||||
final Optional<FilterHolder> filterHolder = filters.stream().filter(filter -> filterClass.equals(filter.getHeldClass())).findFirst();
|
||||
assertTrue(filterHolder.isPresent(), String.format("Filter Class [%s] not found", filterClass));
|
||||
}
|
||||
|
||||
private NiFiProperties getProperties(final Map<String, String> properties) {
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue