Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.server.connector.FrameworkServerConnectorFactory;
import org.apache.nifi.web.server.handler.ContextPathRedirectPatternRule;
import org.apache.nifi.web.server.handler.HeaderWriterHandler;
import org.apache.nifi.web.server.log.RequestLogProvider;
import org.apache.nifi.web.server.log.StandardRequestLogProvider;
Expand All @@ -36,6 +37,7 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -68,7 +70,8 @@ public Server getServer(final NiFiProperties properties) {
server.setHandler(standardHandler);

final RewriteHandler defaultRewriteHandler = new RewriteHandler();
final RedirectPatternRule redirectDefault = new RedirectPatternRule(ALL_PATHS_PATTERN, FRONTEND_CONTEXT_PATH);
final List<String> allowedContextPaths = properties.getAllowedContextPathsAsList();
final RedirectPatternRule redirectDefault = new ContextPathRedirectPatternRule(ALL_PATHS_PATTERN, FRONTEND_CONTEXT_PATH, allowedContextPaths);
defaultRewriteHandler.addRule(redirectDefault);
server.setDefaultHandler(defaultRewriteHandler);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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.handler;

import org.apache.nifi.web.servlet.shared.ProxyHeader;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.Rule;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

/**
* Context Path extension of Redirect Pattern Rule supporting context paths provided in request headers
*/
public class ContextPathRedirectPatternRule extends RedirectPatternRule {

private static final String EMPTY_PATH = "";

private static final String ROOT_PATH = "/";

private final List<String> allowedContextPaths;

/**
* Context Path Redirect Pattern Rule with supported context paths
*
* @param pattern Path pattern to be matched
* @param location Location for redirect URI
* @param allowedContextPaths Context Path values allowed in request headers
*/
public ContextPathRedirectPatternRule(final String pattern, final String location, final List<String> allowedContextPaths) {
super(pattern, location);
this.allowedContextPaths = Objects.requireNonNull(allowedContextPaths, "Allowed Context Paths required");
}

@Override
public Rule.Handler apply(Rule.Handler input) throws IOException {
return new Rule.Handler(input) {
protected boolean handle(Response response, Callback callback) {
final String redirectUri = getRedirectUri(input);
response.setStatus(ContextPathRedirectPatternRule.this.getStatusCode());
response.getHeaders().put(HttpHeader.LOCATION, redirectUri);
callback.succeeded();
return true;
}
};
}

private String getRedirectUri(final Rule.Handler inputHandler) {
final HttpFields requestHeaders = inputHandler.getHeaders();
final String contextPath = getContextPath(requestHeaders);
final String location = getLocation();
final String contextPathLocation = contextPath + location;
return Response.toRedirectURI(inputHandler, contextPathLocation);
}

private String getContextPath(final HttpFields requestHeaders) {
final String path;

final String headerPath = getFirstHeader(requestHeaders, 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 HttpFields requestFields, final ProxyHeader... proxyHeaders) {
return Arrays.stream(proxyHeaders)
.map(ProxyHeader::getHeader)
.map(requestFields::get)
.filter(Objects::nonNull)
.filter(Predicate.not(String::isBlank))
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.handler;

import org.apache.nifi.web.servlet.shared.ProxyHeader;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
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 java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ContextPathRedirectPatternRuleTest {

private static final String PATTERN = "/*";

private static final String LOCATION = "/nifi";

private static final String CONTEXT_PATH = "/context";

private static final String STANDARD_URI = "https://localhost:8443";

private static final String STANDARD_LOCATION = STANDARD_URI + LOCATION;

private static final String CONTEXT_PATH_LOCATION = STANDARD_URI + CONTEXT_PATH + LOCATION;

@Mock
private Request request;

@Mock
private Response response;

@Mock
private Callback callback;

@Mock
private Server server;

@Mock
private HttpFields requestHeaders;

@Mock
private ConnectionMetaData connectionMetaData;

@Mock
private HttpConfiguration httpConfiguration;

private ContextPathRedirectPatternRule rule;

@BeforeEach
void setRule() {
rule = new ContextPathRedirectPatternRule(PATTERN, LOCATION, List.of(CONTEXT_PATH));
}

@Test
void testHandleStandardLocation() throws Exception {
final RewriteHandler rewriteHandler = new RewriteHandler();
rewriteHandler.addRule(rule);
rewriteHandler.setServer(server);
rewriteHandler.start();

final HttpFields.Mutable responseHeaders = setRequest();

rewriteHandler.handle(request, response, callback);

final String location = responseHeaders.get(HttpHeader.LOCATION);
assertEquals(STANDARD_LOCATION, location);
}

@Test
void testHandleContextPathLocation() throws Exception {
final RewriteHandler rewriteHandler = new RewriteHandler();
rewriteHandler.addRule(rule);
rewriteHandler.setServer(server);
rewriteHandler.start();

final HttpFields.Mutable responseHeaders = setRequest();
when(requestHeaders.get(eq(ProxyHeader.PROXY_CONTEXT_PATH.getHeader()))).thenReturn(CONTEXT_PATH);

rewriteHandler.handle(request, response, callback);

final String location = responseHeaders.get(HttpHeader.LOCATION);
assertEquals(CONTEXT_PATH_LOCATION, location);
}

private HttpFields.Mutable setRequest() {
final HttpURI uri = HttpURI.from(STANDARD_URI);
when(request.getHttpURI()).thenReturn(uri);

when(request.getHeaders()).thenReturn(requestHeaders);
when(request.getConnectionMetaData()).thenReturn(connectionMetaData);
when(connectionMetaData.getHttpConfiguration()).thenReturn(httpConfiguration);

final HttpFields.Mutable responseHeaders = HttpFields.build();
when(response.getHeaders()).thenReturn(responseHeaders);

return responseHeaders;
}
}
Loading