Skip to content
Merged
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 @@ -24,4 +24,12 @@ public interface UrlRewriteFilterApplyDescriptor

UrlRewriteFilterApplyDescriptor rule( String rule );

default UrlRewriteFilterApplyMatch match() {
return UrlRewriteFilterApplyMatch.WHOLE;
}

default UrlRewriteFilterApplyDescriptor match( UrlRewriteFilterApplyMatch match ) {
throw new UnsupportedOperationException( "match is not supported by " + getClass().getName() );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.knox.gateway.filter.rewrite.api;

/**
* How the path of an apply is matched against the value it is applied to (attribute value, text node).
*/
public enum UrlRewriteFilterApplyMatch {
/** The path has to match the whole value, the whole value is rewritten. This is the default. */
WHOLE,
/**
* The path is searched for within the value and every match is rewritten on its own, the rest of the value is
* left untouched.
*/
PARTIAL
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
package org.apache.knox.gateway.filter.rewrite.impl;

import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyMatch;

import java.util.Locale;

public class UrlRewriteFilterApplyDescriptorImpl
extends UrlRewriteFilterSelectorDescriptorBase<UrlRewriteFilterApplyDescriptor>
implements UrlRewriteFilterApplyDescriptor {

private String rule;
private UrlRewriteFilterApplyMatch match;

@Override
public String rule() {
Expand All @@ -44,4 +48,27 @@ public String getRule() {
return rule;
}

@Override
public UrlRewriteFilterApplyMatch match() {
return match == null ? UrlRewriteFilterApplyMatch.WHOLE : match;
}

@Override
public UrlRewriteFilterApplyDescriptor match( UrlRewriteFilterApplyMatch match ) {
setMatch( match );
return this;
}

public void setMatch( UrlRewriteFilterApplyMatch match ) {
this.match = match;
}

public void setMatch( String match ) {
this.match = Enum.valueOf( UrlRewriteFilterApplyMatch.class, match.trim().toUpperCase( Locale.ROOT ) );
}

public String getMatch() {
return match == null ? null : match.name().toLowerCase( Locale.ROOT );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.regex.Pattern;

import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyMatch;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterContentDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterPathDescriptor;
Expand Down Expand Up @@ -60,13 +61,46 @@ public static UrlRewriteFilterContentDescriptor getRewriteFilterConfig(

public static String filterJavaScript( String inputValue, UrlRewriteFilterContentDescriptor config,
UrlRewriteFilterReader filterReader, UrlRewriteFilterPathDescriptor.Compiler<Pattern> regexCompiler ) {
return filterMatches( inputValue, config, filterReader, regexCompiler, false );
}

/**
* Rewrites every match of the applies that are configured with match="partial", the rest of the input is left
* untouched. Applies with match="whole" have to match a whole value, they are not used here.
*/
public static String filterPartialMatches( String inputValue, UrlRewriteFilterContentDescriptor config,
UrlRewriteFilterReader filterReader, UrlRewriteFilterPathDescriptor.Compiler<Pattern> regexCompiler ) {
return filterMatches( inputValue, config, filterReader, regexCompiler, true );
}

public static boolean hasPartialMatchApply( UrlRewriteFilterContentDescriptor config ) {
if( config != null ) {
for( UrlRewriteFilterPathDescriptor selector : config.getSelectors() ) {
if( selector instanceof UrlRewriteFilterApplyDescriptor && ((UrlRewriteFilterApplyDescriptor)selector).match() == UrlRewriteFilterApplyMatch.PARTIAL ) {
return true;
}
}
}
return false;
}

/*
* The applies are processed one after the other and every apply searches the output of the previous one, so an

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The applies are chained: each find apply searches the output of the previous one, so with multiple find applies a value can get rewritten twice (e.g. /app -> /gateway/topo/app, then a second find apply matching /gateway/... rewrites it again). That was already the case for <script> content, but with this change it applies to every HTML attribute and text node. Could the dev guide state that find paths must be disjoint/anchored (or should a value be rewritten by at most one find apply)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the dev guide option.

Applies are chained on purpose, the same as for JS content, so there's one behavior to document.

I've added a note to the dev guide that several partial applies are chained and their paths must be disjoint/anchored, plus a complete example with a rule and an anchored path.
If a real need for several overlapping partial applies comes up, "rewrite each part at most once" can be added then; it would only change configs that already rewrite twice.

* apply can match something that was produced by the rewrite of an earlier apply.
*/
private static String filterMatches( String inputValue, UrlRewriteFilterContentDescriptor config,
UrlRewriteFilterReader filterReader, UrlRewriteFilterPathDescriptor.Compiler<Pattern> regexCompiler,
boolean partialOnly ) {
StringBuilder tbuff = new StringBuilder();
StringBuffer sbuff = new StringBuffer();
sbuff.append( inputValue );
if( config != null && !config.getSelectors().isEmpty() ) {
for( UrlRewriteFilterPathDescriptor selector : config.getSelectors() ) {
if ( selector instanceof UrlRewriteFilterApplyDescriptor ) {
UrlRewriteFilterApplyDescriptor apply = (UrlRewriteFilterApplyDescriptor)selector;
if( partialOnly && apply.match() != UrlRewriteFilterApplyMatch.PARTIAL ) {
continue;
}
Matcher matcher = apply.compiledPath( regexCompiler ).matcher( sbuff );
int index = 0;
while ( matcher.find() ) {
Expand All @@ -89,4 +123,5 @@ public static String filterJavaScript( String inputValue, UrlRewriteFilterConten
}
return sbuff.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import net.htmlparser.jericho.StreamedSource;
import net.htmlparser.jericho.Tag;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyMatch;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterContentDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterPathDescriptor;
import org.apache.knox.gateway.filter.rewrite.i18n.UrlRewriteMessages;
Expand Down Expand Up @@ -63,6 +64,7 @@ public abstract class HtmlFilterReaderBase extends Reader implements
private StringWriter writer;
private StringBuffer buffer;
private UrlRewriteFilterContentDescriptor config;
private boolean hasPartialMatchApply;

protected HtmlFilterReaderBase( Reader reader ) throws IOException {
this.reader = reader;
Expand All @@ -77,6 +79,7 @@ protected HtmlFilterReaderBase( Reader reader ) throws IOException {
protected HtmlFilterReaderBase( Reader reader, UrlRewriteFilterContentDescriptor config ) throws IOException, ParserConfigurationException {
this(reader);
this.config = config;
this.hasPartialMatchApply = UrlRewriteUtil.hasPartialMatchApply( config );
}

protected abstract String filterAttribute( QName elementName, QName attributeName, String attributeValue, String ruleName );
Expand Down Expand Up @@ -181,6 +184,10 @@ private void processAttribute( Attribute attribute ) {
if( outputValue == null ) {
outputValue = inputValue;
}
if( name == null && hasPartialMatchApply && outputValue.equals( inputValue ) ) {
// not rewritten as a whole, rewrite the matches of the partial match applies within the value
outputValue = UrlRewriteUtil.filterPartialMatches( inputValue, config, this, REGEX_COMPILER );
}
} catch ( Exception e ) {
LOG.failedToFilterAttribute( attribute.getName(), e );
}
Expand All @@ -196,6 +203,10 @@ private String getRuleName(String inputValue) {
for( UrlRewriteFilterPathDescriptor<?> selector : config.getSelectors() ) {
if ( selector instanceof UrlRewriteFilterApplyDescriptor) {
UrlRewriteFilterApplyDescriptor apply = (UrlRewriteFilterApplyDescriptor)selector;
if( apply.match() == UrlRewriteFilterApplyMatch.PARTIAL ) {
// searched for within the value, see processAttribute and processText
continue;
}
Matcher matcher = apply.compiledPath( REGEX_COMPILER ).matcher( inputValue );
if (matcher.matches()) {
return apply.rule();
Expand All @@ -216,7 +227,12 @@ private void processText( Segment segment ) {
// embedded javascript content
outputValue = UrlRewriteUtil.filterJavaScript( inputValue, config, this, REGEX_COMPILER );
} else {
outputValue = filterText( stack.peek().getQName(), inputValue, getRuleName(inputValue) );
String ruleName = getRuleName(inputValue);
outputValue = filterText( stack.peek().getQName(), inputValue, ruleName );
if( ruleName == null && hasPartialMatchApply && ( outputValue == null || outputValue.equals( inputValue ) ) ) {
// not rewritten as a whole, rewrite the matches of the partial match applies within the text
outputValue = UrlRewriteUtil.filterPartialMatches( inputValue, config, this, REGEX_COMPILER );
}
}
}
if( outputValue == null ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
*/
package org.apache.knox.gateway.filter.rewrite.api;

import org.apache.commons.digester3.ExtendedBaseRules;
import org.apache.commons.digester3.binder.DigesterLoader;
import org.apache.knox.gateway.filter.rewrite.impl.xml.XmlRewriteRulesDigester;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.xml.sax.SAXException;
import org.xmlmatchers.transform.XmlConverters;

import javax.xml.transform.Source;
Expand Down Expand Up @@ -258,4 +262,56 @@ public void testLoadStoreCompleteFilterFile() throws IOException {
assertThat( xml, hasXPath( "/rules/filter/content/buffer/detect/apply/@path", equalTo( "test-apply-path-4" ) ) );
assertThat( xml, hasXPath( "/rules/filter/content/buffer/detect/apply/@rule", equalTo( "test-apply-rule-4" ) ) );
}

@Test
public void testLoadStoreApplyMatch() throws IOException {
String rules =
"<rules><filter name=\"test-filter\"><content type=\"*/html\">" +
"<apply path=\"test-path-1\" rule=\"test-rule-1\"/>" +
"<apply path=\"test-path-2\" rule=\"test-rule-2\" match=\"partial\"/>" +
"<apply path=\"test-path-3\" rule=\"test-rule-3\" match=\"whole\"/>" +
"<apply path=\"test-path-4\" rule=\"test-rule-4\" match=\" PARTIAL \"/>" +
"</content></filter></rules>";
UrlRewriteRulesDescriptor config = UrlRewriteRulesDescriptorFactory.load( "xml", new java.io.StringReader( rules ) );
List<UrlRewriteFilterPathDescriptor> selectors = config.getFilter( "test-filter" ).getContent( "*/html" ).getSelectors();
assertThat( selectors.size(), is( 4 ) );
assertApplyMatches( selectors );

StringWriter writer = new StringWriter();
UrlRewriteRulesDescriptorFactory.store( config, "xml", writer );
Source xml = XmlConverters.the( writer.toString() );
assertThat( xml, hasXPath( "count(/rules/filter/content/apply[@match])", equalTo( "3" ) ) );
assertThat( xml, hasXPath( "/rules/filter/content/apply[@path='test-path-2']/@match", equalTo( "partial" ) ) );
assertThat( xml, hasXPath( "/rules/filter/content/apply[@path='test-path-3']/@match", equalTo( "whole" ) ) );
assertThat( xml, hasXPath( "/rules/filter/content/apply[@path='test-path-4']/@match", equalTo( "partial" ) ) );

UrlRewriteRulesDescriptor reloaded = UrlRewriteRulesDescriptorFactory.load( "xml", new java.io.StringReader( writer.toString() ) );
assertApplyMatches( reloaded.getFilter( "test-filter" ).getContent( "*/html" ).getSelectors() );
}

private static void assertApplyMatches( List<UrlRewriteFilterPathDescriptor> selectors ) {
assertThat( ((UrlRewriteFilterApplyDescriptor)selectors.get( 0 )).match(), is( UrlRewriteFilterApplyMatch.WHOLE ) );
assertThat( ((UrlRewriteFilterApplyDescriptor)selectors.get( 1 )).match(), is( UrlRewriteFilterApplyMatch.PARTIAL ) );
assertThat( ((UrlRewriteFilterApplyDescriptor)selectors.get( 2 )).match(), is( UrlRewriteFilterApplyMatch.WHOLE ) );
assertThat( ((UrlRewriteFilterApplyDescriptor)selectors.get( 3 )).match(), is( UrlRewriteFilterApplyMatch.PARTIAL ) );
}

@Test( expected = SAXException.class )
public void testLoadApplyInvalidMatch() throws IOException, SAXException {
String rules =
"<rules><filter name=\"test-filter\"><content type=\"*/html\">" +
"<apply path=\"test-path-1\" rule=\"test-rule-1\" match=\"bogus\"/>" +
"</content></filter></rules>";
Logger logger = (Logger)LogManager.getLogger( "org.apache.commons.digester3.Digester" );
Level level = logger.getLevel();
try {
logger.setLevel( Level.OFF );
// a failed parse leaves the stack of the digester behind, use an own one instead of the shared one of
// UrlRewriteRulesDescriptorFactory so that the other tests are not affected
DigesterLoader.newLoader( new XmlRewriteRulesDigester() ).newDigester( new ExtendedBaseRules() )
.parse( new java.io.StringReader( rules ) );
} finally {
logger.setLevel( level );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,43 @@ public void testResponseHtmlAttributeEscaping() throws Exception {
assertThat(content, is(rewrittenResponseHtml));
}

/*
* URLs embedded in an entity encoded JSON attribute value and in style text are rewritten one by one.
* @see KNOX-3556
*/
@Test
public void testResponseHtmlBodyRewritePartialMatch() throws Exception {
final Map<String, String> initParams = new HashMap<>();
initParams.put("response.body", "test-filter-partial-match");
testSetUp(initParams);

final String responseHtml = "<html><head>"
+ "<style>@font-face{src:url('/ctx/fonts/a.woff') format('woff'),url(/ctx/fonts/b.woff)}</style>"
+ "<osd-injected-metadata data=\"{&quot;basePath&quot;:&quot;/ctx&quot;,&quot;serverBasePath&quot;:&quot;/ctx&quot;,"
+ "&quot;i18n&quot;:{&quot;translationsUrl&quot;:&quot;/ctx/translations/en.json&quot;}}\"></osd-injected-metadata>"
+ "</head><body><p>see /ctx/docs</p></body></html>";
final String rewrittenResponseHtml = "<html><head>"
+ "<style>@font-face{src:url('/gateway/topo/ctx/fonts/a.woff') format('woff'),url(/gateway/topo/ctx/fonts/b.woff)}</style>"
+ "<osd-injected-metadata data=\"{&quot;basePath&quot;:&quot;/gateway/topo/ctx&quot;,&quot;serverBasePath&quot;:&quot;/gateway/topo/ctx&quot;,"
+ "&quot;i18n&quot;:{&quot;translationsUrl&quot;:&quot;/gateway/topo/ctx/translations/en.json&quot;}}\"></osd-injected-metadata>"
+ "</head><body><p>see /ctx/docs</p></body></html>";

interaction.expect().method("GET")
.requestUrl("http://mock-host:42/test-output-path-1");
interaction.respond().contentType("application/html")
.content(responseHtml, StandardCharsets.UTF_8).status(200);
interactions.add(interaction);
request.setMethod("GET");
request.setURI("/test-input-path");
request.setHeader("Host", "mock-host:42");
request.setHeader("Content-Type", "application/html");

response = TestUtils.execute(server, request);

assertThat(response.getStatus(), is(200));
assertThat(response.getContent(), is(rewrittenResponseHtml));
}

private static class SetupFilter implements Filter {
@Override
public void init( FilterConfig filterConfig ) {
Expand Down
Loading
Loading