Skip to content

Commit

Permalink
NIFI-13594 Added web-servlet-shared abstracted from web-utils (apache…
Browse files Browse the repository at this point in the history
…#9123)

- Moved RequestUriBuilder from web-utils to web-servlet-shared
- Refactored proxy header parsing from WebUtils to StandardRequestUriProvider
- Renamed WebUtils to WebClientUtils

This closes apache#9123
  • Loading branch information
exceptionfactory authored Jul 30, 2024
1 parent f262e74 commit 33da346
Show file tree
Hide file tree
Showing 43 changed files with 895 additions and 782 deletions.
32 changes: 32 additions & 0 deletions nifi-commons/nifi-web-servlet-shared/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-servlet-shared</artifactId>
<description>Shared classes for handling HTTP Servlet requests and responses</description>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${servlet-api.version}</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.servlet.shared;

/**
* Enumeration of supported Proxy Headers that provide information about the original request properties
*/
public enum ProxyHeader {
HOST("Host"),

PROXY_CONTEXT_PATH("X-ProxyContextPath"),

PROXY_SCHEME("X-ProxyScheme"),

PROXY_HOST("X-ProxyHost"),

PROXY_PORT("X-ProxyPort"),

FORWARDED_CONTEXT("X-Forwarded-Context"),

FORWARDED_PREFIX("X-Forwarded-Prefix"),

FORWARDED_PROTO("X-Forwarded-Proto"),

FORWARDED_HOST("X-Forwarded-Host"),

FORWARDED_PORT("X-Forwarded-Port");

private final String header;

ProxyHeader(final String header) {
this.header = header;
}

public String getHeader() {
return header;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.util;

import org.apache.commons.lang3.StringUtils;
package org.apache.nifi.web.servlet.shared;

import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -72,12 +70,9 @@ public static RequestUriBuilder fromHttpServletRequest(final HttpServletRequest
* @return Request URI Builder
*/
public static RequestUriBuilder fromHttpServletRequest(final HttpServletRequest httpServletRequest, final List<String> allowedContextPaths) {
final String scheme = StringUtils.defaultIfEmpty(WebUtils.determineProxiedScheme(httpServletRequest), httpServletRequest.getScheme());
final String host = WebUtils.determineProxiedHost(httpServletRequest);
final int port = WebUtils.getServerPort(httpServletRequest);
final String contextPath = WebUtils.determineContextPath(httpServletRequest);
WebUtils.verifyContextPath(allowedContextPaths, contextPath);
return new RequestUriBuilder(scheme, host, port, contextPath);
final RequestUriProvider requestUriProvider = new StandardRequestUriProvider(allowedContextPaths);
final URI requestUri = requestUriProvider.getRequestUri(httpServletRequest);
return new RequestUriBuilder(requestUri.getScheme(), requestUri.getHost(), requestUri.getPort(), requestUri.getPath());
}

/**
Expand Down Expand Up @@ -109,7 +104,7 @@ public RequestUriBuilder fragment(final String fragment) {
* @throws IllegalArgumentException Thrown on URI syntax exceptions
*/
public URI build() {
final String resourcePath = StringUtils.join(contextPath, path);
final String resourcePath = getResourcePath();
try {
return new URI(scheme, null, host, port, resourcePath, null, fragment);
} catch (final URISyntaxException e) {
Expand All @@ -118,9 +113,29 @@ public URI build() {
}

private static List<String> getAllowedContextPathsConfigured(final HttpServletRequest httpServletRequest) {
final List<String> allowedContextPathsConfigured;

final ServletContext servletContext = httpServletRequest.getServletContext();
final String allowedContextPathsParameter = servletContext.getInitParameter(ALLOWED_CONTEXT_PATHS_PARAMETER);
final String[] allowedContextPathsParsed = StringUtils.split(allowedContextPathsParameter, COMMA_SEPARATOR);
return allowedContextPathsParsed == null ? Collections.emptyList() : Arrays.asList(allowedContextPathsParsed);
if (allowedContextPathsParameter == null) {
allowedContextPathsConfigured = Collections.emptyList();
} else {
final String[] allowedContextPathsParsed = allowedContextPathsParameter.split(COMMA_SEPARATOR);
allowedContextPathsConfigured = Arrays.asList(allowedContextPathsParsed);
}

return allowedContextPathsConfigured;
}

private String getResourcePath() {
final String resourcePath;

if (path == null) {
resourcePath = contextPath;
} else {
resourcePath = contextPath + path;
}

return resourcePath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.servlet.shared;

import jakarta.servlet.http.HttpServletRequest;

import java.net.URI;

/**
* Abstraction for resolving and returning an HTTP Request URI based on presented headers and configured properties
*/
public interface RequestUriProvider {
/**
* Get Request URI from HTTP Servlet Request containing optional headers
*
* @param request HTTP Servlet Request
* @return Request URI
*/
URI getRequestUri(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* 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.servlet.shared;

import jakarta.servlet.http.HttpServletRequest;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Standard implementation of Request URI Provider with allowed hosts and context paths
*/
public class StandardRequestUriProvider implements RequestUriProvider {
private static final Pattern HOST_PATTERN = Pattern.compile("^([^:]+):?([1-9][0-9]{2,4})?$");

private static final Pattern HOST_PORT_REQUIRED_PATTERN = Pattern.compile("^[^:]+:([1-9][0-9]{2,4})$");

private static final Pattern SCHEME_PATTERN = Pattern.compile("^https?$");

private static final int FIRST_GROUP = 1;

private static final int MINIMUM_PORT_NUMBER = 100;

private static final int MAXIMUM_PORT_NUMBER = 65535;

private static final String EMPTY_PATH = "";

private static final String ROOT_PATH = "/";

private final List<String> allowedContextPaths;

public StandardRequestUriProvider(final List<String> allowedContextPaths) {
this.allowedContextPaths = Objects.requireNonNull(allowedContextPaths);
}

/**
* Get Request URI from HTTP Servlet Request containing optional headers and validated against allowed context paths
*
* @param request HTTP Servlet Request
* @return Request URI
*/
@Override
public URI getRequestUri(final HttpServletRequest request) {
Objects.requireNonNull(request, "HTTP Servlet Request required");

final String scheme = getScheme(request);
final String host = getHost(request);
final int port = getPort(request);
final String path = getPath(request);

try {
return new URI(scheme, null, host, port, path, null, null);
} catch (final URISyntaxException e) {
throw new IllegalArgumentException("Request URI construction failed", e);
}
}

private String getScheme(final HttpServletRequest request) {
final String scheme;

final String requestScheme = request.getScheme();
final String headerScheme = getFirstHeader(request, ProxyHeader.PROXY_SCHEME, ProxyHeader.FORWARDED_PROTO);
if (headerScheme == null) {
scheme = requestScheme;
} else {
final Matcher matcher = SCHEME_PATTERN.matcher(headerScheme);
if (matcher.matches()) {
scheme = headerScheme;
} else {
scheme = requestScheme;
}
}

return scheme;
}

private String getHost(final HttpServletRequest request) {
final String host;

final String serverName = request.getServerName();
final String headerHost = getFirstHeader(request, ProxyHeader.PROXY_HOST, ProxyHeader.FORWARDED_HOST, ProxyHeader.HOST);
if (headerHost == null) {
host = serverName;
} else {
final Matcher matcher = HOST_PATTERN.matcher(headerHost);
if (matcher.matches()) {
host = matcher.group(FIRST_GROUP);
} else {
host = serverName;
}
}

return host;
}

private int getPort(final HttpServletRequest request) {
final int port;

final int serverPort = request.getServerPort();
final String headerHost = getFirstHeader(request, ProxyHeader.PROXY_HOST, ProxyHeader.FORWARDED_HOST);
if (headerHost == null) {
port = getProxyPort(request);
} else {
final Matcher matcher = HOST_PORT_REQUIRED_PATTERN.matcher(headerHost);
if (matcher.matches()) {
final String headerPort = matcher.group(FIRST_GROUP);
port = getParsedPort(headerPort, serverPort);
} else {
port = getProxyPort(request);
}
}

return port;
}

private int getProxyPort(final HttpServletRequest request) {
final int port;

final int serverPort = request.getServerPort();
final String headerPort = getFirstHeader(request, ProxyHeader.PROXY_PORT, ProxyHeader.FORWARDED_PORT);
if (headerPort == null) {
port = serverPort;
} else {
port = getParsedPort(headerPort, serverPort);
}

return port;
}

private int getParsedPort(final String headerPort, final int serverPort) {
int port;

try {
final int parsedPort = Integer.parseInt(headerPort);
if (parsedPort < MINIMUM_PORT_NUMBER || parsedPort > MAXIMUM_PORT_NUMBER) {
port = serverPort;
} else {
port = parsedPort;
}
} catch (final Exception e) {
port = serverPort;
}

return port;
}

private String getPath(final HttpServletRequest request) {
final String path;

final String headerPath = getFirstHeader(request, ProxyHeader.PROXY_CONTEXT_PATH, ProxyHeader.FORWARDED_CONTEXT, ProxyHeader.FORWARDED_PREFIX);
if (headerPath == null) {
path = EMPTY_PATH;
} else if (ROOT_PATH.equals(headerPath)) {
path = ROOT_PATH;
} else {
if (allowedContextPaths.contains(headerPath)) {
path = headerPath;
} else {
throw new IllegalArgumentException("Request Header Context Path not allowed based on properties [nifi.web.proxy.context.path]");
}
}

return path;
}

private String getFirstHeader(final HttpServletRequest request, final ProxyHeader... proxyHeaders) {
return Arrays.stream(proxyHeaders)
.map(ProxyHeader::getHeader)
.map(request::getHeader)
.filter(Objects::nonNull)
.filter(Predicate.not(String::isBlank))
.findFirst()
.orElse(null);
}
}
Loading

0 comments on commit 33da346

Please sign in to comment.