/**
* WS-Attacker - A Modular Web Services Penetration Testing Framework Copyright
* (C) 2013 Christian Mainka
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package wsattacker.library.xmlutilities.dom;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.log4j.Logger;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.URI_NS_WSU;
import wsattacker.library.xmlutilities.namespace.NamespaceResolver;
public final class DomUtilities
{
private final static Logger LOG = Logger.getLogger( DomUtilities.class );
private final static String YES = "yes";
/**
* Returns a valid FastXPath that would match the given Node in its Document.
*
* @param node
* @return FastXPath String
*/
public static String getFastXPath( Node node )
{
StringBuffer buf = new StringBuffer();
Element ele;
switch ( node.getNodeType() )
{
case Node.ELEMENT_NODE:
ele = (Element) node;
break;
case Node.ATTRIBUTE_NODE:
buf.append( "/@" );
buf.append( node.getNodeName() );
buf.append( "=\"" );
buf.append( node.getTextContent() );
buf.append( '"' );
ele = ( (Attr) node ).getOwnerElement();
break;
case Node.TEXT_NODE:
buf.append( "/text()" );
ele = (Element) node.getParentNode();
break;
default:
throw new IllegalArgumentException( String.format( "Node '%s' is of Type %s", node.getNodeName(),
node.getNodeType() ) );
}
int index = getElementIndex( ele );
buf.append( ele.getNodeName() + "[" + index + "]" );
Node parent = ele.getParentNode();
// while (parent != ele.getOwnerDocument())
while ( parent != null && parent.getNodeType() == Node.ELEMENT_NODE )
{
index = getElementIndex( (Element) parent );
buf.insert( 0, parent.getNodeName() + "[" + index + "]/" );
parent = parent.getParentNode();
}
buf.insert( 0, "/" );
return buf.toString();
}
/**
* Returns the index of the Node within the current sub-tree. Mainly used for creating FastXPath expressions (
* {@link #getFastXPath(Node)})
*
* @param ele
* @return
*/
public static int getElementIndex( Element ele )
{
int index = 1;
Node prev = ele.getPreviousSibling();
while ( prev != null )
{
if ( prev.getNodeType() == Node.ELEMENT_NODE )
{
if ( ( (Element) prev ).getNodeName().equals( ele.getNodeName() ) )
{
++index;
}
}
prev = prev.getPreviousSibling();
}
return index;
}
/**
* Transforms a List<Node> to a List<String>. Each String in the List is a FastXPath that matches the corresponding
* Node ( {@link #getFastXPath(Node)}.
*
* @param nodelist
* @return
*/
public static List<String> nodelistToFastXPathList( List<? extends Node> nodelist )
{
List<String> fastXPathList = new ArrayList<String>();
for ( Node n : nodelist )
{
fastXPathList.add( getFastXPath( n ) );
}
return fastXPathList;
}
/**
* Takes a Document and evaluates an XPath expression on it. All matching Nodes are returned as a List.
*
* @param doc The Document.
* @param path The XPath expression.
* @return List<Node> that match the XPath.
* @throws XPathExpressionException
*/
public static List<? extends Node> evaluateXPath( Document doc, String path )
throws XPathExpressionException
{
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext( new NamespaceResolver( doc ) );
XPathExpression expr = xpath.compile( path );
NodeList nodes = (NodeList) expr.evaluate( doc, XPathConstants.NODESET );
List<Node> nodelist = new ArrayList<Node>();
for ( int i = 0; i < nodes.getLength(); ++i )
{
nodelist.add( nodes.item( i ) );
}
LOG.trace( String.format( "Evaluated XPath: %s and found %s", path, nodeListToString( nodelist ) ) );
return nodelist;
}
/**
* Finds an Element by its ID name. Looks for wsu:Id.
*
* @param doc
* @param id : ID to search. Does not start with a Hash (#) Sign.
* @return
*/
public static List<Element> findElementByWsuId( Document doc, String id )
{
// String xpath = "//*[@wsu:Id='"+id+"']"; // very basic
// String xpath = "//*[attribute::wsu:Id='"+id+"']"; // expanded from
// basic
String xpath =
String.format( "//attribute::*[local-name()='Id' and namespace-uri()='%s' and string()='%s']/parent::node()",
URI_NS_WSU, id ); // independent
List<Element> result = new ArrayList<Element>();
try
{
result = (List<Element>) evaluateXPath( doc, xpath );
LOG.trace( String.format( "### WSU IDs found ### \n%s", nodelistToFastXPathList( result ) ) );
}
catch ( XPathExpressionException e )
{
LOG.warn( String.format( "BAD XPath: %s", xpath ) );
}
finally
{
// nothing to do
}
return result;
}
/**
* Finds an Element by its ID name. Looks for wsu:Id.
*
* @param doc
* @param attributeValue : ID to search. Does not start with a Hash (#) Sign.
* @return
*/
public static List<Attr> findAttributeByValue( Document doc, String attributeValue )
{
List<Attr> returnedElements = new ArrayList<Attr>();
String xpath = String.format( "//attribute::*[string()='%s']", attributeValue ); // for corresponding node:
// /parent::node()";
try
{
returnedElements = (List<Attr>) evaluateXPath( doc, xpath );
LOG.trace( String.format( "### Element with Attribute '%s' ### \n%s", attributeValue,
nodelistToFastXPathList( returnedElements ) ) );
}
catch ( XPathExpressionException e )
{
LOG.warn( String.format( "BAD XPATH: %s", xpath ) );
}
return returnedElements;
}
/**
* Returns the first child Element of a given Node.
*
* @param node
* @return Child Element.
*/
public static Element getFirstChildElement( Node node )
{
Node child = node.getFirstChild();
while ( ( child != null ) && ( child.getNodeType() != Node.ELEMENT_NODE ) )
{
child = child.getNextSibling();
}
return (Element) child;
}
/**
* Returns the next sibling element of a Node.
*
* @param node
* @return Next Sibling
*/
public static Element getNextSiblingElement( Node node )
{
Node sibling = node.getNextSibling();
while ( ( sibling != null ) && ( sibling.getNodeType() != Node.ELEMENT_NODE ) )
{
sibling = sibling.getNextSibling();
}
return (Element) sibling;
}
/**
* Finds child elements that matches a given prefix:localname.
*
* @param parent The parent Node
* @param localname Localename of the child, null for matching every child
* @param namespaceuri Namespace-URI of the child, null for matching every namespace
* @return List<Element>
*/
public static List<Element> findChildren( Node parent, String localname, String namespaceuri )
{
return findChildren( parent, localname, namespaceuri, false );
}
/**
* Finds child elements that matches a given prefix:localname.
*
* @param parent The parent Node
* @param localname Localename of the child, null for matching every child
* @param namespaceuri Namespace-URI of the child, null for matching very namespace
* @param deep if true, the search will be recursive with child elements
* @return List<Element>
*/
public static List<Element> findChildren( Node parent, String localname, String namespaceuri, boolean deep )
{
List<Element> matches = new ArrayList<Element>();
findChildren( matches, parent, localname, namespaceuri, deep );
return matches;
}
/**
* Get all child elements of Element ele as a List of Elements. Non recursive.
*
* @param ele
* @return
*/
public static List<Element> getAllChildElements( Element ele )
{
return getAllChildElements( ele, false );
}
/**
* Get all child elements of Element ele as a List of Elements. If recursive is true, even the child elements of all
* childelements are fetched recursively.
*
* @param ele
* @param recursive
* @return
*/
public static List<Element> getAllChildElements( Element ele, boolean recursive )
{
List<Element> list = new ArrayList<Element>();
return getAllChildElements( ele, recursive, list );
}
/**
* Creates an empty Document
*
* @return
*/
public static Document createDomDocument()
{
Document doc = null;
try
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware( true );
DocumentBuilder builder = factory.newDocumentBuilder();
doc = builder.newDocument();
}
catch ( ParserConfigurationException e )
{
throw new IllegalStateException(
String.format( "%s.createDomDucment() could not instantiate DocumentBuilderFactory. This should never happen",
DomUtilities.class.getName() ), e );
}
finally
{
// nothing to do
}
return doc;
}
/**
* Creates a new Docume1nt with the Node toClone as root Element Can be used to clone a Document with
* createNewDomFromNode(rootToClone)
*
* @param toClone
* @return
*/
public static Document createNewDomFromNode( Node toClone )
{
Document newDoc = createDomDocument(); // empty Doc
Node importedNode = newDoc.importNode( toClone, true ); // import node to
// clone, deep
// copy
newDoc.appendChild( importedNode ); // the clone
return newDoc;
}
// *****************************************************************
// Read / Write XML
public static Document readDocument( String filename )
throws FileNotFoundException, SAXException, IOException
{
File file = new File( filename );
return readDocument( file );
}
public static Document readDocument( File file )
throws FileNotFoundException, SAXException, IOException
{
return readDocument( new FileInputStream( file ) );
}
public static Document readDocument( URL url )
throws SAXException, IOException
{
URLConnection con = url.openConnection();
con.setConnectTimeout( 1000 );
con.setReadTimeout( 1000 );
con.setUseCaches( true );
return readDocument( url.openStream() );
}
/**
* Reads an XML file and returns a Document.
*
* @param file
* @return Document
* @throws ParserConfigurationException
* @throws FileNotFoundException
* @throws SAXException
* @throws IOException
*/
public static Document readDocument( InputStream is )
throws SAXException, IOException
{
DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
fac.setNamespaceAware( true );
// fac.setIgnoringElementContentWhitespace(true);
DocumentBuilder builder = null;
try
{
builder = fac.newDocumentBuilder();
}
catch ( ParserConfigurationException e )
{
throw new IllegalStateException(
String.format( "%s.readDocument() could not instantiate DocumentBuilderFactory. This should never happen",
DomUtilities.class.getName() ), e );
}
return builder.parse( is );
}
/**
* Writes a Document to a file.
*
* @param doc
* @param filename
*/
public static void writeDocument( Document doc, String filename )
{
writeDocument( doc, filename, false );
}
public static void writeDocument( Document doc, String filename, boolean prettyPrint )
{
try
{
TransformerFactory tf = TransformerFactory.newInstance();
tf.setFeature( XMLConstants.FEATURE_SECURE_PROCESSING, true );
Transformer trans = tf.newTransformer();
if ( prettyPrint )
{
trans.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, YES );
trans.setOutputProperty( OutputKeys.INDENT, YES );
}
trans.transform( new DOMSource( doc ), new StreamResult( new FileOutputStream( filename ) ) );
}
catch ( Exception e )
{
e.printStackTrace();
}
}
// *****************************************************************
// String/DOM Conversation
public static Document stringToDom( String xmlString )
throws SAXException
{
return stringToDom( xmlString, true );
}
// *****************************************************************
// String/DOM Conversation
public static Document stringToDom( String xmlString, boolean namespaceAwareness )
throws SAXException
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
factory.setNamespaceAware( namespaceAwareness );
try
{
factory.setFeature( XMLConstants.FEATURE_SECURE_PROCESSING, true );
builder = factory.newDocumentBuilder();
}
catch ( ParserConfigurationException e )
{
throw new IllegalStateException(
String.format( "%s.stringToDom() could not instantiate DocumentBuilderFactory. This should never happen",
DomUtilities.class.getName() ), e );
}
StringReader reader = new StringReader( xmlString );
InputSource input = new InputSource( reader );
Document dom;
try
{
dom = builder.parse( input );
}
catch ( IOException e )
{
// will never happen
dom = DomUtilities.createDomDocument();
}
return dom;
}
/**
* Converts a DOM to a String
*
* @param domDoc
* @return
*/
public static String domToString( Document domDoc )
{
return domToString( domDoc.getDocumentElement(), false );
}
public static String domToString( Document domDoc, boolean prettyPrint )
{
return domToString( domDoc.getDocumentElement(), prettyPrint );
}
/**
* Converts a DOM Node to a String
*
* @param Node n
* @return
*/
public static String domToString( Node n )
{
return domToString( n, false );
}
public static String domToString( Node n, boolean prettyPrint )
{
StringWriter output = new StringWriter();
Transformer transformer = null;
try
{
transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, YES );
if ( prettyPrint )
{
transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "2" );
transformer.setOutputProperty( OutputKeys.INDENT, YES );
}
transformer.transform( new DOMSource( n ), new StreamResult( output ) );
}
catch ( Exception e )
{
throw new IllegalStateException(
String.format( "%s.domToString() throws an Exception. This should never happen",
DomUtilities.class.getName() ), e );
}
return output.toString();
}
public static String showOnlyImportant( Document doc )
{
return showOnlyImportant( doc.getDocumentElement() );
}
public static String showOnlyImportant( Node node )
{
return showOnlyImportant( domToString( node, true ) );
}
public static String showOnlyImportant( String xml )
{
// Filter out specific elements
String[] tasks = { "ds:KeyInfo", "ds:SignatureValue", "ds:SignedInfo" };
for ( String task : tasks )
{
// xml = xml.replaceAll("<" + task + "(.|\\n)*<\\/" + task + ">",
// "<" + task + "/>");
for ( int start = xml.indexOf( "<" + task ); start >= 0; start = xml.indexOf( "<" + task ) )
{
String endString = "/" + task + ">";
int end = xml.indexOf( endString, start + 1 );
if ( end > 0 )
{
xml = xml.substring( 0, start ) + "<" + task + "/>" + xml.substring( end + endString.length() );
}
else
{
break;
}
}
}
// Filter out namespace declarations
// xml = xml.replaceAll("\\sxmlns:\\S+['\"]", "");
String nsOpen = " xmlns:";
for ( int start = xml.indexOf( nsOpen ); start >= 0; start = xml.indexOf( nsOpen ) )
{
int nextWhitespace = xml.indexOf( ' ', start + 1 );
int nextClose = xml.indexOf( '>', start + 1 );
if ( nextClose < nextWhitespace && nextClose >= 0 )
{
xml = xml.substring( 0, start ) + xml.substring( nextClose );
}
else if ( nextWhitespace >= 0 )
{
xml = xml.substring( 0, start ) + xml.substring( nextWhitespace );
}
else
{
break;
}
}
return xml;
}
/**
* * PrettyPrints a List<Node>
*
* @param list
* @return
*/
public static String nodeListToString( List<? extends Node> list )
{
StringBuffer buf = new StringBuffer();
buf.append( "{" );
if ( list.size() > 0 )
{
buf.append( "\n 0 : [" + domToString( list.get( 0 ) ) + "]" );
for ( int i = 1; i < list.size(); ++i )
{
buf.append( "\n " + i + " : [" + domToString( list.get( 0 ) ) + "]" );
}
buf.append( "\n" );
}
buf.append( "}" );
return buf.toString();
}
/**
* The Element element is not part of the Document doc. This Function returns the corresponding Element in doc. If
* it does not exist yet, it will be created.
*
* @param doc
* @param element
* @return
*/
public static Element findCorrespondingElement( Document doc, Element element )
{
List<Element> parentElements = new ArrayList<Element>();
List<Integer> parentIndex = new ArrayList<Integer>();
// First: Add each parent node to a temporary list
// (Go upstairs to root beginning with element)
Node theParent = element;
while ( theParent != null && theParent.getNodeType() == Node.ELEMENT_NODE )
{
parentElements.add( (Element) theParent );
parentIndex.add( getElementIndex( (Element) theParent ) );
theParent = theParent.getParentNode();
}
// Second: Travel the list of parents in reverse order
// (Go downstairs from root to the corresponding element)
Element ret = doc.getDocumentElement();
if ( ret.isSameNode( element.getOwnerDocument().getDocumentElement() ) )
{
LOG.warn( "No different Root Nodes" );
}
// -2 as root should be the same
for ( int i = ( parentElements.size() - 2 ); i >= 0; --i )
{
Element child = parentElements.get( i );
int index = parentIndex.get( i );
// NodeList children =
// ret.getElementsByTagNameNS(child.getNamespaceURI(),
// child.getLocalName()); // Bad: This is recursive
List<Element> children = findChildren( ret, child.getLocalName(), child.getNamespaceURI() ); // Non-Recursive
if ( index > children.size() )
{
// create nodes if not exist
for ( int j = 0; j < ( index - children.size() ); ++j )
{
Node imported = doc.importNode( child, false );
ret.appendChild( imported );
}
// re-get children
// children =
// ret.getElementsByTagNameNS(child.getNamespaceURI(),
// child.getLocalName());
children = findChildren( ret, child.getLocalName(), child.getNamespaceURI() );
}
// ret = (Element) children.item(index - 1); // Index of Node is 1
// based, lists start with element 0
ret = children.get( index - 1 ); // Index of Node is 1 based, lists
// start with element 0
}
return ret;
}
public static String getNamespaceURI( Node node, String searchPrefix )
{
return node.lookupNamespaceURI( searchPrefix );
}
/**
* Searches for a prefix of the given searchNamespaceURI parameter.
*
* @param node
* @param searchNamespaceURI
* @return return thePrefix, if searchNamespaceURI is in the scope of the node and a prefix is declared, returns the
* empty String, if the searchNamespaceURI is the defaultNamespace, or null, if no declration is found.
*/
public static String getPrefix( Node node, String searchNamespaceURI )
{
String prefix;
if ( node.isDefaultNamespace( searchNamespaceURI ) )
{
prefix = "";
}
else
{
prefix = node.lookupPrefix( searchNamespaceURI );
}
return prefix;
}
public static Element getFirstChildElementByNames( Element startElement, String... localname )
{
Element foundElement = startElement;
for ( int i = 0; i < localname.length; ++i )
{
String tmp = localname[i];
try
{
foundElement = DomUtilities.findChildren( foundElement, tmp, null, false ).get( 0 );
}
catch ( NullPointerException e )
{
throw new IllegalArgumentException(
String.format( "Could not find Element with name '%s' (%d Parameter)",
tmp, i ) );
}
}
return foundElement;
}
private static void findChildren( List<Element> result, Node parent, String localname, String namespaceuri,
boolean deep )
{
if ( LOG.isTraceEnabled() )
{
LOG.trace( String.format( "From %s find children %s with URI %s%s", parent.getNodeName(), localname,
namespaceuri, deep ? " DEEP" : " nondeep" ) );
}
NodeList children = parent.getChildNodes();
for ( int i = 0; i < children.getLength(); ++i )
{
Node n = children.item( i );
if ( LOG.isTraceEnabled() )
{
LOG.trace( "Found Child: " + children.item( i ).getNodeName() );
}
if ( n != null && n.getNodeType() == Node.ELEMENT_NODE )
{
if ( localname == null || n.getLocalName().equals( localname ) )
{
if ( namespaceuri == null || n.getNamespaceURI().equals( namespaceuri ) )
{
result.add( (Element) n );
}
}
}
}
for ( int i = 0; i < children.getLength(); ++i )
{
Node n = children.item( i );
if ( deep && n.getNodeType() == Node.ELEMENT_NODE )
{
findChildren( result, n, localname, namespaceuri, deep );
}
}
}
private static List<Element> getAllChildElements( Element ele, boolean recursive, List<Element> list )
{
NodeList nl = ele.getChildNodes();
for ( int i = 0; i < nl.getLength(); ++i )
{
Node n = nl.item( i );
if ( n.getNodeType() != Node.ELEMENT_NODE )
{
continue;
}
list.add( (Element) n );
if ( recursive )
{
getAllChildElements( (Element) n, recursive, list );
}
}
return list;
}
private DomUtilities()
{
}
}