org.zaproxy.zap.extension.soap.WSDLCustomParser.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.soap.WSDLCustomParser.java

Source

/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2014 The ZAP Development Team
 *
 * Licensed 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.zaproxy.zap.extension.soap;

import com.predic8.schema.ComplexType;
import com.predic8.schema.Element;
import com.predic8.schema.Schema;
import com.predic8.schema.SimpleType;
import com.predic8.schema.restriction.BaseRestriction;
import com.predic8.schema.restriction.facet.EnumerationFacet;
import com.predic8.wsdl.AbstractBinding;
import com.predic8.wsdl.Binding;
import com.predic8.wsdl.BindingOperation;
import com.predic8.wsdl.Definitions;
import com.predic8.wsdl.Operation;
import com.predic8.wsdl.Part;
import com.predic8.wsdl.Port;
import com.predic8.wsdl.PortType;
import com.predic8.wsdl.Service;
import com.predic8.wsdl.WSDLParser;
import com.predic8.wstool.creator.RequestCreator;
import com.predic8.wstool.creator.SOARequestCreator;
import com.predic8.xml.util.ResourceDownloadException;
import groovy.xml.MarkupBuilder;
import java.awt.EventQueue;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.log4j.Logger;
import org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.extension.history.ExtensionHistory;
import org.parosproxy.paros.model.HistoryReference;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.network.HttpHeader;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpRequestHeader;
import org.parosproxy.paros.network.HttpSender;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.network.HttpRequestBody;

public class WSDLCustomParser {

    private static final Logger LOG = Logger.getLogger(WSDLCustomParser.class);
    private static int keyIndex = -1;
    private ImportWSDL wsdlSingleton = ImportWSDL.getInstance();
    private SOAPMsgConfig lastConfig; // Only used for unit testing purposes.

    public WSDLCustomParser() {
    }

    /* Method called from external classes to import a WSDL file from an URL. */
    public void extUrlWSDLImport(final String url, final String threadName) {
        if (url == null || url.trim().length() <= 0)
            return;
        // LOG.debug("Importing WSDL file from URL: "+url);
        Thread t = new Thread() {
            @Override
            public void run() {
                // Thread name: THREAD_PREFIX + threadId++
                this.setName(threadName);
                parseWSDLUrl(url);
            }
        };
        t.start();
    }

    /*
     * Method called from external classes to import a WSDL file specifying content.
     */
    public boolean extContentWSDLImport(final String content) {
        return parseWSDLContent(content);
    }

    public boolean extContentWSDLImport(final String content, final boolean sendMessages) {
        return parseWSDLContent(content, sendMessages);
    }

    /* Method called from external classes to import a WSDL file from an URL. */
    public void extUrlWSDLImport(final String url) {
        parseWSDLUrl(url);
    }

    /*
     * Method called from external classes to import a WSDL file from a local file.
     */
    public void extFileWSDLImport(final File file, final String threadName) {
        Thread t = new Thread() {
            @Override
            public void run() {
                this.setName(threadName);
                parseWSDLFile(file);
            }
        };
        t.start();
    }

    public boolean canBeWSDLparsed(String content) {
        if (content == null || content.trim().length() <= 0) {
            return false;
        } else {
            // WSDL parsing.
            WSDLParser parser = new WSDLParser();
            try {
                InputStream contentI = new ByteArrayInputStream(content.getBytes("UTF-8"));
                parser.parse(contentI);
                contentI.close();
                return true;
            } catch (Exception e) {
                return false;
            }
        }
    }

    /*
     * Generates WSDL definitions from a WSDL file and then it calls parsing
     * functions.
     */
    private void parseWSDLFile(File file) {
        if (file == null)
            return;
        try {
            if (View.isInitialised()) {
                // Switch to the output panel, if in GUI mode
                View.getSingleton().getOutputPanel().setTabFocus();
            }

            // WSDL file parsing.
            WSDLParser parser = new WSDLParser();
            final String path = file.getAbsolutePath();
            Definitions wsdl = parser.parse(path);
            parseWSDL(wsdl, true);

        } catch (ResourceDownloadException rde) {
            String exMsg = Constant.messages.getString("soap.topmenu.tools.importWSDL.fail",
                    file.getAbsolutePath());
            LOG.warn(exMsg);
            if (View.isInitialised()) {
                View.getSingleton().showWarningDialog(exMsg);
            }
        } catch (Exception e) {
            LOG.error(e.getMessage(), e);
        }
    }

    /*
     * Generates WSDL definitions from a WSDL string and then it calls parsing
     * functions.
     */
    private void parseWSDLUrl(String url) {
        if (url == null || url.trim().equals(""))
            return;
        try {
            if (View.isInitialised()) {
                // Switch to the output panel, if in GUI mode
                try {
                    View.getSingleton().getOutputPanel().setTabFocus();
                } catch (Exception e) {
                    LOG.debug("Could not set tab focus on Output Panel.");
                }
            }
            /* Sends a request to retrieve remote WSDL file's content. */
            HttpMessage httpRequest = new HttpMessage(new URI(url, false));
            HttpSender sender = new HttpSender(Model.getSingleton().getOptionsParam().getConnectionParam(), true,
                    HttpSender.MANUAL_REQUEST_INITIATOR);
            try {
                sender.sendAndReceive(httpRequest, true);
            } catch (IOException e) {
                LOG.error("Unable to send WSDL request.", e);
                return;
            }

            /* Checks response content. */
            if (httpRequest.getResponseBody() != null) {
                String content = httpRequest.getResponseBody().toString();
                if (content == null || content.trim().length() <= 0) {
                    // LOG.warn("URL response from WSDL file request has no body
                    // content.");
                } else {
                    // WSDL parsing.
                    parseWSDLContent(content);
                }
            }
        } catch (Exception e) {
            LOG.error("There was an error while parsing WSDL from URL. ", e);
        }
    }

    private boolean parseWSDLContent(String content, boolean sendMessages) {
        if (content == null || content.trim().length() <= 0) {
            return false;
        } else {
            // WSDL parsing.
            WSDLParser parser = new WSDLParser();
            try {
                InputStream contentI = new ByteArrayInputStream(content.getBytes("UTF-8"));
                Definitions wsdl = parser.parse(contentI);
                contentI.close();
                parseWSDL(wsdl, sendMessages);
                return true;
            } catch (Exception e) {
                LOG.error("There was an error while parsing WSDL content. ", e);
                return false;
            }
        }
    }

    private boolean parseWSDLContent(String content) {
        return parseWSDLContent(content, true);
    }

    /* Parses WSDL definitions and identifies endpoints and operations. */
    private void parseWSDL(Definitions wsdl, boolean sendMessages) {
        StringBuilder sb = new StringBuilder();
        List<Service> services = wsdl.getServices();
        keyIndex++;

        /* Endpoint identification. */
        for (Service service : services) {
            for (Port port : service.getPorts()) {
                Binding binding = port.getBinding();
                AbstractBinding innerBinding = binding.getBinding();
                String soapPrefix = innerBinding.getPrefix();
                int soapVersion = detectSoapVersion(wsdl, soapPrefix); // SOAP 1.X, where X is represented by this
                // variable.
                /* If the binding is not a SOAP binding, it is ignored. */
                String style = detectStyle(innerBinding);
                if (style != null && (style.equals("document") || style.equals("rpc"))) {

                    List<BindingOperation> operations = binding.getOperations();
                    String endpointLocation = port.getAddress().getLocation().toString();
                    sb.append("\n|-- Port detected: " + port.getName() + " (" + endpointLocation + ")\n");

                    /* Identifies operations for each endpoint.. */
                    for (BindingOperation bindOp : operations) {
                        String opDisplayName = "/" + bindOp.getName() + " (v1." + soapVersion + ")";
                        sb.append("|\t|-- SOAP 1." + soapVersion + " Operation: " + bindOp.getName());
                        /* Adds this operation to the global operations chart. */
                        recordOperation(keyIndex, bindOp);
                        /* Identifies operation's parameters. */
                        List<Part> requestParts = detectParameters(wsdl, bindOp);
                        /* Set values to parameters. */
                        HashMap<String, String> formParams = new HashMap<String, String>();
                        for (Part part : requestParts) {
                            Element element = part.getElement();
                            if (element != null) {
                                formParams.putAll(fillParameters(element, null));
                            }
                        }
                        /* Connection test for each operation. */
                        /* Basic message creation. */
                        SOAPMsgConfig soapConfig = new SOAPMsgConfig(wsdl, soapVersion, formParams, port, bindOp);
                        lastConfig = soapConfig;
                        HttpMessage requestMessage = createSoapRequest(soapConfig);
                        if (sendMessages)
                            sendSoapRequest(keyIndex, requestMessage, opDisplayName, sb);
                    } // bindingOperations loop
                } // Binding check if
            } // Ports loop
        }
        printOutput(sb);
    }

    /*
     * Detects SOAP version used in a binding, given the wsdl content and the soap
     * binding prefix.
     */
    private int detectSoapVersion(Definitions wsdl, String soapPrefix) {
        String soapNamespace = wsdl.getNamespace(soapPrefix).toString();
        if (soapNamespace.trim().equals("http://schemas.xmlsoap.org/wsdl/soap12/")) {
            return 2;
        } else {
            return 1;
        }
    }

    private String detectStyle(AbstractBinding binding) {
        try {
            String r = binding.getProperty("style").toString();
            binding.getProperty("transport");
            return r.trim();
        } catch (MissingPropertyExceptionNoStack e) {
            // It has no style or transport property, so it is not a SOAP binding.
            LOG.info("No style or transport property detected", e);
            return null;
        }
    }

    /* Record the given operation in the global chart. */
    private void recordOperation(int wsdlID, BindingOperation bindOp) {
        String soapActionName = "";
        try {
            soapActionName = bindOp.getOperation().getSoapAction();
        } catch (NullPointerException e) {
            // SOAP Action not defined for this operation.
            LOG.info("No SOAP Action defined for this operation.", e);
            return;
        }
        if (!soapActionName.trim().equals("")) {
            wsdlSingleton.putAction(wsdlID, soapActionName);
        }
    }

    private List<Part> detectParameters(Definitions wsdl, BindingOperation bindOp) {
        for (PortType pt : wsdl.getPortTypes()) {
            for (Operation op : pt.getOperations()) {
                if (op.getName().trim().equals(bindOp.getName().trim())) {
                    return op.getInput().getMessage().getParts();
                }
            }
        }
        return null;
    }

    private HashMap<String, String> fillParameters(Element element, String parent) {
        HashMap<String, String> formParams = new HashMap<String, String>();
        try {
            /* Tries to parse it as a complex type first. */
            String xpath = null;
            if (parent != null)
                xpath = parent + "/" + element.getName();
            else
                xpath = element.getName();
            ComplexType ct = (ComplexType) element.getEmbeddedType();
            /* Handles when ComplexType is not embedded but referenced by 'type'. */
            if (ct == null) {
                Schema currentSchema = element.getSchema();
                ct = (ComplexType) currentSchema.getType(element.getType());
                if (ct == null)
                    throw new ClassCastException("Complex Type is null after cast."); // Hashmap is empty here.
            }
            for (Element e : ct.getSequence().getElements()) {
                /* Recursive parsing for nested complex types. */
                formParams.putAll(fillParameters(e, xpath));
            }
        } catch (ClassCastException cce) {
            /* Simple element treatment. */
            if (element == null)
                return formParams;
            /* Handles simple types. */
            SimpleType simpleType = null;
            try {
                simpleType = (SimpleType) element.getEmbeddedType();
                if (simpleType == null) {
                    Schema currentSchema = element.getSchema();
                    simpleType = (SimpleType) currentSchema.getType(element.getType());
                    if (simpleType == null) {
                        /* It is not simple type, so it is treated as a plain element. */
                        String xpath = "";
                        if (parent != null)
                            xpath = parent + "/" + element.getName();
                        else
                            xpath = element.getName();
                        if (element.getType() != null)
                            return addParameter(xpath, element.getType().getQualifiedName(), null);
                        else
                            return formParams;
                    }
                }
            } catch (ClassCastException cce2) {
                /* It is not simple type, so it is treated as a plain element. */
                String xpath = "";
                if (parent != null)
                    xpath = parent + "/" + element.getName();
                else
                    xpath = element.getName();
                return addParameter(xpath, element.getType().getQualifiedName(), null);
            }
            /* Handles enumeration restriction. */
            BaseRestriction br = simpleType.getRestriction();
            if (br != null) {
                List<EnumerationFacet> enums = br.getEnumerationFacets();
                if (enums != null && enums.size() > 0) {
                    String defaultValue = enums.get(0).getValue();
                    formParams.putAll(addParameter(parent + "/" + element.getName(), "string", defaultValue));
                }
            }
            return formParams;
        } catch (Exception e) {
            LOG.warn("There was an error when trying to parse element " + element.getName() + " from WSDL file.",
                    e);
        }
        return formParams;
    }

    private HashMap<String, String> addParameter(String path, String paramType, String value) {
        HashMap<String, String> formParams = new HashMap<String, String>();
        LOG.debug("Detected parameter: " + path);
        if (paramType.contains(":")) {
            String[] stringParts = paramType.split(":");
            paramType = stringParts[stringParts.length - 1];
        }
        /* If value is specified, it is directly set. */
        if (value != null) {
            formParams.put("xpath:/" + path, value);
            return formParams;
        }
        /* Parameter value depends on parameter type. */
        if (paramType.equals("string")) {
            formParams.put("xpath:/" + path, "paramValue");
        } else if (paramType.equals("int") || paramType.equals("double") || paramType.equals("long")) {
            formParams.put("xpath:/" + path, "0");
        } else if (paramType.equals("date")) {
            Date date = new Date();
            SimpleDateFormat dt1 = new SimpleDateFormat("CCYY-MM-DD");
            String dateS = dt1.format(date);
            formParams.put("xpath:/" + path, dateS);
        } else if (paramType.equals("dateTime")) {
            Date date = new Date();
            SimpleDateFormat dt1 = new SimpleDateFormat("CCYY-MM-DDThh:mm:ssZ");
            String dateS = dt1.format(date);
            formParams.put("xpath:/" + path, dateS);
        }
        return formParams;
    }

    /* Generates a SOAP request associated to the specified binding operation. */
    public HttpMessage createSoapRequest(SOAPMsgConfig soapConfig) {
        if (soapConfig == null || !soapConfig.isComplete())
            return null;

        /* Retrieving configuration variables. */
        Definitions wsdl = soapConfig.getWsdl();
        HashMap<String, String> formParams = soapConfig.getParams();
        Port port = soapConfig.getPort();
        int soapVersion = soapConfig.getSoapVersion();
        BindingOperation bindOp = soapConfig.getBindOp();

        /* Start message crafting. */
        StringWriter writerSOAPReq = new StringWriter();

        SOARequestCreator creator = new SOARequestCreator(wsdl, new RequestCreator(),
                new MarkupBuilder(writerSOAPReq));
        creator.setBuilder(new MarkupBuilder(writerSOAPReq));
        creator.setDefinitions(wsdl);
        creator.setFormParams(formParams);
        creator.setCreator(new RequestCreator());

        try {
            Binding binding = port.getBinding();
            creator.createRequest(binding.getPortType().getName(), bindOp.getName(), binding.getName());

            // LOG.info("[ExtensionImportWSDL] "+writerSOAPReq);
            /* HTTP Request. */
            String endpointLocation = port.getAddress().getLocation().toString();
            HttpMessage httpRequest = new HttpMessage(new URI(endpointLocation, false));
            /* Body. */
            HttpRequestBody httpReqBody = httpRequest.getRequestBody();
            /* [MARK] Not sure if all servers would handle this encoding type. */
            httpReqBody.append(
                    "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n" + writerSOAPReq.getBuffer().toString());
            httpRequest.setRequestBody(httpReqBody);
            /* Header. */
            HttpRequestHeader httpReqHeader = httpRequest.getRequestHeader();
            httpReqHeader.setMethod("POST");
            /* Sets headers according to SOAP version. */
            if (soapVersion == 1) {
                httpReqHeader.setHeader(HttpHeader.CONTENT_TYPE, "text/xml; charset=UTF-8");
                httpReqHeader.setHeader("SOAPAction", bindOp.getOperation().getSoapAction());
            } else if (soapVersion == 2) {
                String contentType = "application/soap+xml; charset=UTF-8";
                String action = bindOp.getOperation().getSoapAction();
                if (!action.trim().equals(""))
                    contentType += "; action=" + action;
                httpReqHeader.setHeader(HttpHeader.CONTENT_TYPE, contentType);
            }
            httpReqHeader.setContentLength(httpReqBody.length());
            httpRequest.setRequestHeader(httpReqHeader);
            /* Saves the message and its configuration. */
            wsdlSingleton.putConfiguration(httpRequest, soapConfig);
            return httpRequest;
        } catch (Exception e) {
            LOG.error("Unable to generate request for operation '" + bindOp.getName() + "'\n" + e.getMessage(), e);
            return null;
        }
    }

    /*
     * Sends a given SOAP request. File is needed to record its associated ops, and
     * stringBuilder logs the output message.
     */
    private void sendSoapRequest(int wsdlID, HttpMessage httpRequest, String opDisplayName, StringBuilder sb) {
        if (httpRequest == null)
            return;
        HttpRequestBody body = httpRequest.getRequestBody();
        /* Avoids connection if message has no proper body. */
        if (body == null || body.getBytes().length <= 0)
            return;
        /* Connection. */
        HttpSender sender = new HttpSender(Model.getSingleton().getOptionsParam().getConnectionParam(), true,
                HttpSender.MANUAL_REQUEST_INITIATOR);
        /* Send request. */
        try {
            sender.sendAndReceive(httpRequest, true);
        } catch (IOException e) {
            LOG.error("Unable to communicate with SOAP server. Server may be not available.");
            LOG.debug("Trace:", e);
        }
        wsdlSingleton.putRequest(wsdlID, httpRequest);
        persistMessage(httpRequest, opDisplayName);
        if (sb != null)
            sb.append(" (Status code: " + httpRequest.getResponseHeader().getStatusCode() + ")\n");
    }

    private static void persistMessage(final HttpMessage message, final String opDisplayName) {
        // Add the message to the history panel and sites tree
        final HistoryReference historyRef;

        try {
            historyRef = new HistoryReference(Model.getSingleton().getSession(), HistoryReference.TYPE_ZAP_USER,
                    message);
        } catch (Exception e) {
            LOG.warn(e.getMessage(), e);
            return;
        }

        final ExtensionHistory extHistory = Control.getSingleton().getExtensionLoader()
                .getExtension(ExtensionHistory.class);
        if (extHistory != null) {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    extHistory.addHistory(historyRef);

                    // FIXME
                    /*
                     * Modifies the URI adding the SOAP operation name to avoid overwrites. It's
                     * done after saving it in history so that the original URI is preserved for
                     * scanning tasks.
                     *
                     * URI modification solution is only a workaround since it has size limitations
                     * and, anyway, it is not an elegant solution.
                     */
                    try {
                        URI soapURI = message.getRequestHeader().getURI();
                        String soapStringURI = soapURI.getPath();
                        soapURI.setPath(soapStringURI + opDisplayName);
                    } catch (URIException e) {
                        LOG.warn(e.getMessage(), e);
                    }
                    Model.getSingleton().getSession().getSiteTree().addPath(historyRef, message);
                }
            });
        }
    }

    /* Prints output string in output panel. */
    private void printOutput(StringBuilder sb) {
        if (View.isInitialised()) {
            final String str = sb.toString();
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    View.getSingleton().getOutputPanel().append(str);
                }
            });
        }
    }

    SOAPMsgConfig getLastConfig() {
        return lastConfig;
    }
}