blob: 1b6846e209ec1ec0830be2a82b85a86585ef9285 [file] [log] [blame]
/*
* 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.impl.xml;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterApplyDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterBufferDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterContentDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterDetectDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterGroupDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterPathDescriptor;
import org.apache.knox.gateway.filter.rewrite.api.UrlRewriteFilterScopeDescriptor;
import org.apache.knox.gateway.filter.rewrite.i18n.UrlRewriteResources;
import org.apache.knox.gateway.i18n.resources.ResourcesFactory;
import org.apache.knox.gateway.util.XmlUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.Comment;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.StartDocument;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.Stack;
import java.util.regex.Pattern;
public abstract class XmlFilterReader extends Reader {
private static final UrlRewriteResources RES = ResourcesFactory.get( UrlRewriteResources.class );
private static final String DEFAULT_XML_VERSION = "1.0";
private static final UrlRewriteFilterPathDescriptor.Compiler<XPathExpression> XPATH_COMPILER = new XmlPathCompiler();
private static final UrlRewriteFilterPathDescriptor.Compiler<Pattern> REGEX_COMPILER = new RegexCompiler();
private Reader reader;
private UrlRewriteFilterContentDescriptor config;
private int offset;
private StringWriter writer;
private StringBuffer buffer;
private XMLInputFactory factory;
private XMLEventReader parser;
private Document document;
private Stack<Level> stack;
private boolean isEmptyElement;
protected XmlFilterReader( Reader reader, UrlRewriteFilterContentDescriptor config ) throws IOException, XMLStreamException {
this.reader = reader;
this.config = config;
writer = new StringWriter();
buffer = writer.getBuffer();
offset = 0;
document = null;
stack = new Stack<>();
isEmptyElement = false;
factory = XMLInputFactory.newFactory();
//KNOX-620 factory.setProperty( XMLConstants.ACCESS_EXTERNAL_DTD, Boolean.FALSE );
//KNOX-620 factory.setProperty( XMLConstants.ACCESS_EXTERNAL_SCHEMA, Boolean.FALSE );
/* This disables DTDs entirely for that factory */
factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
/* disable external entities */
factory.setProperty("javax.xml.stream.isSupportingExternalEntities", Boolean.FALSE);
factory.setProperty( "javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE );
factory.setProperty("http://java.sun.com/xml/stream/"
+ "properties/report-cdata-event", Boolean.TRUE);
parser = factory.createXMLEventReader( reader );
}
protected abstract String filterAttribute( QName elementName, QName attributeName, String attributeValue, String ruleName );
protected abstract String filterText( QName elementName, String text, String ruleName );
@Override
public int read( char[] destBuffer, int destOffset, int destCount ) throws IOException {
int count = 0;
int available = buffer.length() - offset;
if( available == 0 ) {
if( parser.hasNext() ) {
try {
XMLEvent event = parser.nextEvent();
processEvent( event );
} catch( IOException | RuntimeException e ) {
throw e;
} catch ( Exception e ) {
throw new RuntimeException( e );
}
available = buffer.length() - offset;
} else {
count = -1;
}
}
if( available > 0 ) {
count = Math.min( destCount, available );
buffer.getChars( offset, offset + count, destBuffer, destOffset );
offset += count;
if( offset == buffer.length() ) {
offset = 0;
buffer.setLength( 0 );
}
}
return count;
}
private void processEvent( XMLEvent event ) throws ParserConfigurationException, XPathExpressionException, IOException, XMLStreamException {
int type = event.getEventType();
switch( type ) {
case XMLStreamConstants.START_DOCUMENT:
processStartDocument( (StartDocument)event );
break;
case XMLStreamConstants.END_DOCUMENT:
processEndDocument();
break;
case XMLStreamConstants.START_ELEMENT:
if( parser.peek().getEventType() == XMLStreamConstants.END_ELEMENT ) {
isEmptyElement = true;
}
processStartElement( event.asStartElement());
break;
case XMLStreamConstants.END_ELEMENT:
processEndElement( event.asEndElement() );
isEmptyElement = false;
break;
case XMLStreamConstants.CHARACTERS:
case XMLStreamConstants.CDATA:
case XMLStreamConstants.SPACE:
processCharacters( event.asCharacters() );
break;
case XMLStreamConstants.COMMENT:
processComment( (Comment)event );
break;
case XMLStreamConstants.DTD:
case XMLStreamConstants.NAMESPACE:
case XMLStreamConstants.ATTRIBUTE:
case XMLStreamConstants.ENTITY_REFERENCE:
case XMLStreamConstants.ENTITY_DECLARATION:
case XMLStreamConstants.NOTATION_DECLARATION:
case XMLStreamConstants.PROCESSING_INSTRUCTION:
default:
// Fail if we run into any of these for now.
throw new IllegalStateException( Integer.toString( type ) );
}
}
private void processStartDocument( StartDocument event ) throws ParserConfigurationException {
String s;
document = XmlUtils.createDocument( false );
pushLevel( null, document, document, config );
writer.write( "<?xml" );
s = event.getVersion();
if( s == null ) {
s = DEFAULT_XML_VERSION;
}
writer.write( " version=\"");
writer.write( s );
writer.write( "\"" );
s = event.getCharacterEncodingScheme();
if( s != null ) {
writer.write( " encoding=\"");
writer.write( s );
writer.write( "\"" );
}
writer.write( " standalone=\"");
writer.write( event.isStandalone() ? "yes" : "no" );
writer.write( "\"" );
writer.write( "?>" );
}
private void processEndDocument() {
stack.clear();
document = null;
}
private void processStartElement( StartElement event ) throws XPathExpressionException {
// Create a new "empty" element and add it to the document.
Element element = bufferElement( event );
Level parent = stack.peek();
parent.node.appendChild( element );
// If already buffering just continue to do so.
// Note: Don't currently support nested buffer or scope descriptors.
if( currentlyBuffering() ) {
pushLevel( parent, element, parent.scopeNode, parent.scopeConfig );
bufferAttributes( event, element );
// Else not currently buffering
} else {
// See if there is a matching path descriptor in the current scope.
UrlRewriteFilterPathDescriptor descriptor = pickFirstMatchingPath( parent );
if( descriptor != null ) {
// If this is a buffer descriptor then switch to buffering and buffer the attributes.
if( descriptor instanceof UrlRewriteFilterBufferDescriptor ) {
pushLevel( parent, element, element, (UrlRewriteFilterBufferDescriptor)descriptor );
bufferAttributes( event, element );
// Otherwise if this is a scope descriptor then change the scope and stream the attributes.
} else if( descriptor instanceof UrlRewriteFilterScopeDescriptor ) {
pushLevel( parent, element, element, (UrlRewriteFilterScopeDescriptor)descriptor );
streamElement( event, element );
// Else found an unexpected matching path.
} else {
// This is likely because there is an <apply> targeted at the text of an element.
// That "convenience" config will be taken care of in the streamElement() processing.
pushLevel( parent, element, parent.scopeNode, parent.scopeConfig );
streamElement( event, element );
}
// If there is no matching path descriptor then continue streaming.
} else {
pushLevel( parent, element, parent.scopeNode, parent.scopeConfig );
streamElement( event, element );
}
}
}
private void processEndElement( EndElement event ) throws XPathExpressionException, IOException {
boolean buffering = currentlyBuffering();
Level child = stack.pop();
if( buffering ) {
if( child.node == child.scopeNode ) {
processBufferedElement( child );
}
} else {
if( ! isEmptyElement ) {
QName n = event.getName();
writer.write( "</" );
String p = n.getPrefix();
if( p != null && !p.isEmpty() ) {
writer.write( p );
writer.write( ":" );
}
writer.write( n.getLocalPart() );
writer.write( ">" );
}
child.node.getParentNode().removeChild( child.node );
}
}
private Element bufferElement( StartElement event ) {
QName qname = event.getName();
String prefix = qname.getPrefix();
String uri = qname.getNamespaceURI();
Element element;
if( uri == null || uri.isEmpty() ) {
element = document.createElement( qname.getLocalPart() );
} else {
element = document.createElementNS( qname.getNamespaceURI(), qname.getLocalPart() );
if( prefix != null && !prefix.isEmpty() ) {
element.setPrefix( prefix );
}
}
// Always need to buffer the namespaces regardless of what else happens so that XPath will work on attributes
// namespace qualified attributes.
bufferNamespaces( event, element );
return element;
}
private void bufferNamespaces( StartElement event, Element element ) {
Iterator namespaces = event.getNamespaces();
while( namespaces.hasNext() ) {
Namespace namespace = (Namespace)namespaces.next();
if( namespace.isDefaultNamespaceDeclaration() ) {
element.setAttribute( "xmlns", namespace.getNamespaceURI() );
} else {
element.setAttribute( "xmlns:" + namespace.getPrefix(), namespace.getNamespaceURI() );
}
}
}
private void streamElement( StartElement event, Element element ) throws XPathExpressionException {
writer.write( "<" );
QName qname = event.getName();
String prefix = event.getName().getPrefix();
if( prefix != null && !prefix.isEmpty() ) {
writer.write( prefix );
writer.write( ":" );
}
writer.write( qname.getLocalPart() );
streamNamespaces( event );
streamAttributes( event, element );
if( isEmptyElement ) {
writer.write("/>");
} else {
writer.write(">");
}
}
private void processBufferedElement( Level level, UrlRewriteFilterGroupDescriptor config ) throws XPathExpressionException {
for( UrlRewriteFilterPathDescriptor selector : config.getSelectors() ) {
if( selector instanceof UrlRewriteFilterApplyDescriptor ) {
XPathExpression path = (XPathExpression)selector.compiledPath( XPATH_COMPILER );
Object node = path.evaluate( level.scopeNode, XPathConstants.NODE );
if( node != null ) {
UrlRewriteFilterApplyDescriptor apply = (UrlRewriteFilterApplyDescriptor)selector;
if( node instanceof Element ) {
Element element = (Element)node;
String value = element.getTextContent();
value = filterText( extractQName( element ), value, apply.rule() );
element.setTextContent( value );
} else if( node instanceof Text ) {
Text text = (Text)node;
String value = text.getWholeText();
value = filterText( extractQName( text.getParentNode() ), value, apply.rule() );
text.replaceWholeText( value );
} else if( node instanceof Attr ) {
Attr attr = (Attr)node;
String value = attr.getValue();
value = filterAttribute( extractQName( attr.getOwnerElement() ), extractQName( attr ), value, apply.rule() );
attr.setValue( value );
} else {
throw new IllegalArgumentException( RES.unexpectedSelectedNodeType( node ) );
}
}
} else if( selector instanceof UrlRewriteFilterDetectDescriptor) {
XPathExpression path = (XPathExpression)selector.compiledPath( XPATH_COMPILER );
Object node = path.evaluate( level.scopeNode, XPathConstants.NODE );
if( node != null ) {
UrlRewriteFilterDetectDescriptor detect = (UrlRewriteFilterDetectDescriptor)selector;
String value;
if( node instanceof Element ) {
Element element = (Element)node;
value = element.getTextContent();
} else if( node instanceof Text ) {
Text text = (Text)node;
value = text.getWholeText();
} else if( node instanceof Attr ) {
Attr attr = (Attr)node;
value = attr.getValue();
} else {
throw new IllegalArgumentException( RES.unexpectedSelectedNodeType( node ) );
}
if( detect.compiledValue( REGEX_COMPILER ).matcher( value ).matches() ) {
processBufferedElement( level, detect );
}
}
} else {
throw new IllegalArgumentException( RES.unexpectedRewritePathSelector( selector ) );
}
}
}
private void processBufferedElement( Level level ) throws XPathExpressionException, IOException {
processBufferedElement( level, level.scopeConfig );
writeBufferedElement( level.node, writer );
}
private QName extractQName( Node node ) {
QName qname;
String localName = node.getLocalName();
if( localName == null ) {
qname = new QName( node.getNodeName() );
} else {
if ( node.getPrefix() == null ) {
qname = new QName( node.getNamespaceURI(), localName );
} else {
qname = new QName( node.getNamespaceURI(), localName, node.getPrefix() );
}
}
return qname;
}
private void bufferAttributes( StartElement event, Element element ) {
Iterator attributes = event.getAttributes();
while( attributes.hasNext() ) {
Attribute attribute = (Attribute)attributes.next();
bufferAttribute( element, attribute );
}
}
private Attr bufferAttribute( Element element, Attribute attribute ) {
QName name = attribute.getName();
String prefix = name.getPrefix();
String uri = name.getNamespaceURI();
Attr node;
if( uri == null || uri.isEmpty() ) {
node = document.createAttribute( name.getLocalPart() );
element.setAttributeNode( node );
} else {
node = document.createAttributeNS( uri, name.getLocalPart() );
if( prefix != null && !prefix.isEmpty() ) {
node.setPrefix( prefix );
}
element.setAttributeNodeNS( node );
}
node.setTextContent( attribute.getValue() );
return node;
}
private void streamNamespaces( StartElement event ) {
Iterator i = event.getNamespaces();
while( i.hasNext() ) {
Namespace ns = (Namespace)i.next();
writer.write( " xmlns" );
if( !ns.isDefaultNamespaceDeclaration() ) {
writer.write( ":" );
writer.write( ns.getPrefix() );
}
writer.write( "=\"" );
writer.write( ns.getNamespaceURI() );
writer.write( "\"" );
}
}
private void streamAttributes( StartElement event, Element element ) throws XPathExpressionException {
Iterator i = event.getAttributes();
while( i.hasNext() ) {
Attribute attribute = (Attribute)i.next();
streamAttribute( element, attribute );
}
}
private void streamAttribute( Element element, Attribute attribute ) throws XPathExpressionException {
Attr node;
QName name = attribute.getName();
String prefix = name.getPrefix();
String uri = name.getNamespaceURI();
if( uri == null || uri.isEmpty() ) {
node = document.createAttribute( name.getLocalPart() );
element.setAttributeNode( node );
} else {
node = document.createAttributeNS( uri, name.getLocalPart() );
if( prefix != null && !prefix.isEmpty() ) {
node.setPrefix( prefix );
}
element.setAttributeNodeNS( node );
}
String value = attribute.getValue();
Level level = stack.peek();
if( ( level.scopeConfig ) == null || ( level.scopeConfig.getSelectors().isEmpty() ) ) {
value = filterAttribute( null, attribute.getName(), value, null );
node.setValue( value );
} else {
UrlRewriteFilterPathDescriptor path = pickFirstMatchingPath( level );
if( path instanceof UrlRewriteFilterApplyDescriptor ) {
String rule = ((UrlRewriteFilterApplyDescriptor)path).rule();
value = filterAttribute( null, attribute.getName(), value, rule );
node.setValue( value );
}
}
if( prefix == null || prefix.isEmpty() ) {
writer.write( " " );
writer.write( name.getLocalPart() );
} else {
writer.write( " " );
writer.write( prefix );
writer.write( ":" );
writer.write( name.getLocalPart() );
}
writer.write( "=\"" );
writer.write( value );
writer.write( "\"" );
element.removeAttributeNode( node );
}
private void processCharacters( Characters event ) {
Level level = stack.peek();
Node node = stack.peek().node;
if( event.isCData() ) {
node.appendChild( document.createCDATASection( event.getData() ) );
} else {
node.appendChild( document.createTextNode( event.getData() ) );
}
if( !currentlyBuffering() ) {
String value = event.getData();
if( !event.isWhiteSpace() ) {
if( level.scopeConfig == null || level.scopeConfig.getSelectors().isEmpty() ) {
value = filterText( extractQName( node ), value, null );
} else {
UrlRewriteFilterPathDescriptor path = pickFirstMatchingPath( level );
if( path instanceof UrlRewriteFilterApplyDescriptor ) {
String rule = ((UrlRewriteFilterApplyDescriptor)path).rule();
value = filterText( extractQName( node ), value, rule );
}
}
}
if( event.isCData() ) {
writer.write( "<![CDATA[" );
writer.write( value );
writer.write( "]]>" );
} else {
writer.write( StringEscapeUtils.escapeXml11( value ) );
}
}
}
private void processComment( Comment event ) {
if( currentlyBuffering() ) {
stack.peek().node.appendChild( document.createComment( event.getText() ) );
} else {
writer.write( "<!--" );
writer.write( event.getText() );
writer.write( "-->" );
}
}
@Override
public void close() throws IOException {
try {
parser.close();
} catch( XMLStreamException e ) {
throw new IOException( e );
}
reader.close();
writer.close();
stack.clear();
}
protected UrlRewriteFilterPathDescriptor pickFirstMatchingPath( Level level ) {
UrlRewriteFilterPathDescriptor match = null;
if( level.scopeConfig != null ) {
for( UrlRewriteFilterPathDescriptor selector : level.scopeConfig.getSelectors() ) {
try {
XPathExpression path = (XPathExpression)selector.compiledPath( XPATH_COMPILER );
Object node = path.evaluate( level.scopeNode, XPathConstants.NODE );
if( node != null ) {
match = selector;
break;
}
} catch( XPathExpressionException e ) {
throw new IllegalArgumentException( selector.path(), e );
}
}
}
return match;
}
private boolean currentlyBuffering() {
return stack.peek().buffered;
}
private Level pushLevel( Level parent, Node node, Node scopeNode, UrlRewriteFilterGroupDescriptor scopeConfig ) {
Level level = new Level( parent, node, scopeNode, scopeConfig );
stack.push( level );
return level;
}
private static class Level {
private Node node;
private UrlRewriteFilterGroupDescriptor scopeConfig;
private Node scopeNode;
private boolean buffered;
Level( Level parent, Node node, Node scopeNode, UrlRewriteFilterGroupDescriptor scopeConfig ) {
this.node = node;
this.scopeConfig = scopeConfig;
this.scopeNode = scopeNode;
this.buffered = ( parent != null && parent.buffered ) ||
(scopeConfig instanceof UrlRewriteFilterBufferDescriptor);
}
}
private static class XmlPathCompiler implements UrlRewriteFilterPathDescriptor.Compiler<XPathExpression> {
private static final XPathFactory xpathFactory = getXpathFactory();
private static synchronized XPathFactory getXpathFactory() {
XPathFactory xPathFactory = XPathFactory.newInstance();
try {
xPathFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
} catch (XPathFactoryConfigurationException ex) {
// ignore
}
return xPathFactory;
}
private synchronized XPathExpression getXPathExpression(String expression) {
try {
return xpathFactory.newXPath().compile(expression);
} catch (XPathExpressionException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public XPathExpression compile( String expression, XPathExpression compiled ) {
if(compiled != null) {
return compiled;
} else {
return getXPathExpression(expression);
}
}
}
private static class RegexCompiler implements UrlRewriteFilterPathDescriptor.Compiler<Pattern> {
@Override
public Pattern compile( String expression, Pattern compiled ) {
if(compiled != null) {
return compiled;
} else {
return Pattern.compile( expression );
}
}
}
private static void writeBufferedElement( Node node, Writer writer ) throws IOException {
try {
Transformer t = XmlUtils.getTransformer( false, false, 0, true );
t.transform( new DOMSource( node ), new StreamResult( writer ) );
} catch( TransformerException e ) {
throw new IOException( e );
}
}
}