org.zaproxy.zap.extension.api.API.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.api.API.java

Source

/*
 * Zed Attack Proxy (ZAP) and its related class files.
 * 
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 * 
 * 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.api;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.httpclient.URIException;
import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;
import org.parosproxy.paros.core.proxy.ProxyParam;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.network.HttpHeader;
import org.parosproxy.paros.network.HttpInputStream;
import org.parosproxy.paros.network.HttpMalformedHeaderException;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpOutputStream;
import org.parosproxy.paros.network.HttpRequestHeader;
import org.parosproxy.paros.view.View;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import net.sf.json.JSONObject;

public class API {
    public enum Format {
        XML, HTML, JSON, JSONP, UI, OTHER
    };

    public enum RequestType {
        action, view, other, pconn
    };

    /**
     * The custom domain to access the ZAP API while proxying through ZAP.
     * 
     * @see #getBaseURL(boolean)
     */
    public static final String API_DOMAIN = "zap";

    /**
     * The HTTP URL to access the ZAP API while proxying through ZAP.
     * 
     * @see #getBaseURL(boolean)
     */
    public static final String API_URL = "http://" + API_DOMAIN + "/";

    /**
     * The HTTPS URL to access the ZAP API while proxying through ZAP.
     * 
     * @see #getBaseURL(boolean)
     */
    public static final String API_URL_S = "https://" + API_DOMAIN + "/";
    public static final String API_KEY_PARAM = "apikey";
    public static final String API_NONCE_PARAM = "apinonce";

    private static Pattern patternParam = Pattern.compile("&", Pattern.CASE_INSENSITIVE);
    private static final String CALL_BACK_URL = "/zapCallBackUrl/";

    private static final String STATUS_OK = "200 OK";
    private static final String STATUS_BAD_REQUEST = "400 Bad Request";
    private static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error";

    private Map<String, ApiImplementor> implementors = new HashMap<>();
    private static API api = null;
    private WebUI webUI = new WebUI(this);
    private Map<String, ApiImplementor> callBacks = new HashMap<>();

    private Map<String, ApiImplementor> shortcuts = new HashMap<>();

    private Map<String, Nonce> nonces = Collections.synchronizedMap(new HashMap<String, Nonce>());

    /**
     * The options for the API.
     * 
     * @see #getOptionsParamApi()
     */
    private OptionsParamApi optionsParamApi;

    /**
     * The options of the local proxy.
     * 
     * @see #getProxyParam()
     */
    private ProxyParam proxyParam;

    private Random random = new SecureRandom();
    private static final Logger logger = Logger.getLogger(API.class);

    private static synchronized API newInstance() {
        if (api == null) {
            api = new API();
        }
        return api;
    }

    public static API getInstance() {
        if (api == null) {
            newInstance();
        }
        return api;
    }

    /**
     * Registers the given {@code ApiImplementor} to the ZAP API.
     * <p>
     * The implementor is not registed if the {@link ApiImplementor#getPrefix() API implementor prefix} is already in use.
     * <p>
     * <strong>Note:</strong> The preferred method to add an {@code ApiImplementor} is through the method
     * {@link org.parosproxy.paros.extension.ExtensionHook#addApiImplementor(ApiImplementor)
     * ExtensionHook.addApiImplementor(ApiImplementor)} when the corresponding
     * {@link org.parosproxy.paros.extension.Extension#hook(org.parosproxy.paros.extension.ExtensionHook) extension is hooked}.
     * Only use this method if really necessary.
     *
     * @param impl the implementor that will be registered
     * @see #removeApiImplementor(ApiImplementor)
     */
    public void registerApiImplementor(ApiImplementor impl) {
        if (implementors.get(impl.getPrefix()) != null) {
            logger.error("Second attempt to register API implementor with prefix of " + impl.getPrefix());
            return;
        }
        implementors.put(impl.getPrefix(), impl);
        for (String shortcut : impl.getApiShortcuts()) {
            logger.debug("Registering API shortcut: " + shortcut);
            if (this.shortcuts.containsKey(shortcut)) {
                logger.error("Duplicate API shortcut: " + shortcut);
            }
            this.shortcuts.put("/" + shortcut, impl);
        }
    }

    /**
     * Removes the given {@code ApiImplementor} from the ZAP API.
     *
     * @param impl the implementor that will be removed
     * @since 2.1.0
     * @see #registerApiImplementor(ApiImplementor)
     */
    public void removeApiImplementor(ApiImplementor impl) {
        if (!implementors.containsKey(impl.getPrefix())) {
            logger.warn("Attempting to remove an API implementor not registered, with prefix: " + impl.getPrefix());
            return;
        }
        implementors.remove(impl.getPrefix());
        for (String shortcut : impl.getApiShortcuts()) {
            String key = "/" + shortcut;
            if (this.shortcuts.containsKey(key)) {
                logger.debug("Removing registered API shortcut: " + shortcut);
                this.shortcuts.remove(key);
            }
        }
    }

    public boolean isEnabled() {
        // Check API is enabled (its always enabled if run from the cmdline)
        if (View.isInitialised() && !getOptionsParamApi().isEnabled()) {
            return false;
        }
        return true;
    }

    private OptionsParamApi getOptionsParamApi() {
        if (optionsParamApi == null) {
            optionsParamApi = Model.getSingleton().getOptionsParam().getApiParam();
        }
        return optionsParamApi;
    }

    void setOptionsParamApi(OptionsParamApi optionsParamApi) {
        this.optionsParamApi = optionsParamApi;
    }

    private ProxyParam getProxyParam() {
        if (proxyParam == null) {
            proxyParam = Model.getSingleton().getOptionsParam().getProxyParam();
        }
        return proxyParam;
    }

    void setProxyParam(ProxyParam proxyParam) {
        this.proxyParam = proxyParam;
    }

    public boolean handleApiRequest(HttpRequestHeader requestHeader, HttpInputStream httpIn,
            HttpOutputStream httpOut) throws IOException {
        return this.handleApiRequest(requestHeader, httpIn, httpOut, false);
    }

    private boolean isPermittedAddr(HttpRequestHeader requestHeader) {
        if (getOptionsParamApi().isPermittedAddress(requestHeader.getSenderAddress().getHostAddress())) {
            if (getOptionsParamApi().isPermittedAddress(requestHeader.getHostName())) {
                return true;
            }
            logger.warn("Request to API URL " + requestHeader.getURI().toString() + " with host header "
                    + requestHeader.getHostName() + " not permitted");
            return false;
        }
        logger.warn("Request to API URL " + requestHeader.getURI().toString() + " from "
                + requestHeader.getSenderAddress().getHostAddress() + " not permitted");
        return false;
    }

    public boolean handleApiRequest(HttpRequestHeader requestHeader, HttpInputStream httpIn,
            HttpOutputStream httpOut, boolean force) throws IOException {

        String url = requestHeader.getURI().toString();
        Format format = Format.OTHER;
        ApiImplementor callbackImpl = null;
        ApiImplementor shortcutImpl = null;

        // Check for callbacks
        if (url.contains(CALL_BACK_URL)) {
            if (!isPermittedAddr(requestHeader)) {
                return true;
            }
            logger.debug("handleApiRequest Callback: " + url);
            for (Entry<String, ApiImplementor> callback : callBacks.entrySet()) {
                if (url.startsWith(callback.getKey())) {
                    callbackImpl = callback.getValue();
                    break;
                }
            }
        }
        String path = requestHeader.getURI().getPath();
        if (path != null) {
            for (Entry<String, ApiImplementor> shortcut : shortcuts.entrySet()) {
                if (path.startsWith(shortcut.getKey())) {
                    shortcutImpl = shortcut.getValue();
                    break;
                }
            }
        }

        if (shortcutImpl == null && callbackImpl == null && !url.startsWith(API_URL) && !url.startsWith(API_URL_S)
                && !force) {
            return false;
        }
        if (!isPermittedAddr(requestHeader)) {
            return true;
        }
        if (getOptionsParamApi().isSecureOnly() && !requestHeader.isSecure()) {
            // Insecure request with secure only set, always ignore
            logger.debug("handleApiRequest rejecting insecure request");
            return true;
        }

        logger.debug("handleApiRequest " + url);

        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader(requestHeader);
        if (requestHeader.getContentLength() > 0) {
            msg.setRequestBody(httpIn.readRequestBody(requestHeader));
        }
        String component = null;
        ApiImplementor impl = null;
        RequestType reqType = null;
        String contentType = "text/plain; charset=UTF-8";
        String response = "";
        String name = null;
        boolean error = false;

        try {
            JSONObject params = getParams(requestHeader.getURI().getEscapedQuery());

            if (shortcutImpl != null) {
                if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) {
                    if (!this.hasValidKey(requestHeader, params)) {
                        throw new ApiException(ApiException.Type.BAD_API_KEY);
                    }
                }
                msg = shortcutImpl.handleShortcut(msg);
            } else if (callbackImpl != null) {
                // Callbacks have suitably random URLs and therefore don't require keys/nonces
                response = callbackImpl.handleCallBack(msg);
            } else {

                // Parse the query:
                // format of url is http://zap/format/component/reqtype/name/?params
                //                    0  1  2    3        4        5      6
                String[] elements = url.split("/");

                if (elements.length > 3 && elements[3].equalsIgnoreCase("favicon.ico")) {
                    // Treat the favicon as a special case:)
                    if (!getOptionsParamApi().isUiEnabled()) {
                        throw new ApiException(ApiException.Type.DISABLED);
                    }
                    InputStream is = API.class.getResourceAsStream("/resource/zap.ico");
                    byte[] icon = new byte[is.available()];
                    is.read(icon);
                    is.close();

                    msg.setResponseHeader(getDefaultResponseHeader(contentType));
                    msg.getResponseHeader().setContentLength(icon.length);
                    httpOut.write(msg.getResponseHeader());
                    httpOut.write(icon);
                    httpOut.flush();
                    httpOut.close();
                    httpIn.close();
                    return true;

                } else if (elements.length > 3) {
                    try {
                        format = Format.valueOf(elements[3].toUpperCase());
                        switch (format) {
                        case JSON:
                            contentType = "application/json; charset=UTF-8";
                            break;
                        case JSONP:
                            contentType = "application/javascript; charset=UTF-8";
                            break;
                        case XML:
                            contentType = "text/xml; charset=UTF-8";
                            break;
                        case HTML:
                            contentType = "text/html; charset=UTF-8";
                            break;
                        case UI:
                            contentType = "text/html; charset=UTF-8";
                            break;
                        default:
                            break;
                        }
                    } catch (IllegalArgumentException e) {
                        format = Format.HTML;
                        throw new ApiException(ApiException.Type.BAD_FORMAT);
                    }
                }
                if (elements.length > 4) {
                    component = elements[4];
                    impl = implementors.get(component);
                    if (impl == null) {
                        throw new ApiException(ApiException.Type.NO_IMPLEMENTOR);
                    }
                }
                if (elements.length > 5) {
                    try {
                        reqType = RequestType.valueOf(elements[5]);
                    } catch (IllegalArgumentException e) {
                        throw new ApiException(ApiException.Type.BAD_TYPE);
                    }
                }
                if (elements.length > 6) {
                    name = elements[6];
                    if (name != null && name.indexOf("?") > 0) {
                        name = name.substring(0, name.indexOf("?"));
                    }
                }

                if (format.equals(Format.UI)) {
                    if (!isEnabled() || !getOptionsParamApi().isUiEnabled()) {
                        throw new ApiException(ApiException.Type.DISABLED);
                    }

                    response = webUI.handleRequest(component, impl, reqType, name);
                    contentType = "text/html; charset=UTF-8";
                } else if (name != null) {
                    if (!isEnabled()) {
                        throw new ApiException(ApiException.Type.DISABLED);
                    }
                    // Do this now as it might contain the api key/nonce
                    if (requestHeader.getMethod().equalsIgnoreCase(HttpRequestHeader.POST)) {
                        String contentTypeHeader = requestHeader.getHeader(HttpHeader.CONTENT_TYPE);
                        if (contentTypeHeader != null
                                && contentTypeHeader.equals(HttpHeader.FORM_URLENCODED_CONTENT_TYPE)) {
                            params = getParams(msg.getRequestBody().toString());
                        } else {
                            throw new ApiException(ApiException.Type.CONTENT_TYPE_NOT_SUPPORTED);
                        }
                    }

                    if (format.equals(Format.JSONP)) {
                        if (!getOptionsParamApi().isEnableJSONP()) {
                            // Not enabled
                            throw new ApiException(ApiException.Type.DISABLED);
                        }
                        if (!this.hasValidKey(requestHeader, params)) {
                            // An api key is required for ALL JSONP requests
                            throw new ApiException(ApiException.Type.BAD_API_KEY);
                        }
                    }

                    ApiResponse res;
                    switch (reqType) {
                    case action:
                        if (!getOptionsParamApi().isDisableKey()) {
                            if (!this.hasValidKey(requestHeader, params)) {
                                throw new ApiException(ApiException.Type.BAD_API_KEY);
                            }
                        }

                        ApiAction action = impl.getApiAction(name);

                        if (action != null) {
                            // Checking for null to handle option actions
                            List<String> mandatoryParams = action.getMandatoryParamNames();
                            if (mandatoryParams != null) {
                                for (String param : mandatoryParams) {
                                    if (!params.has(param) || params.getString(param).length() == 0) {
                                        throw new ApiException(ApiException.Type.MISSING_PARAMETER, param);
                                    }
                                }
                            }
                        }

                        res = impl.handleApiOptionAction(name, params);
                        if (res == null) {
                            res = impl.handleApiAction(name, params);
                        }
                        switch (format) {
                        case JSON:
                            response = res.toJSON().toString();
                            break;
                        case JSONP:
                            response = this.getJsonpWrapper(res.toJSON().toString());
                            break;
                        case XML:
                            response = this.responseToXml(name, res);
                            break;
                        case HTML:
                            response = this.responseToHtml(name, res);
                            break;
                        default:
                            break;
                        }

                        break;
                    case view:
                        if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) {
                            if (!this.hasValidKey(requestHeader, params)) {
                                throw new ApiException(ApiException.Type.BAD_API_KEY);
                            }
                        }
                        ApiView view = impl.getApiView(name);
                        if (view != null) {
                            // Checking for null to handle option actions
                            List<String> mandatoryParams = view.getMandatoryParamNames();
                            if (mandatoryParams != null) {
                                for (String param : mandatoryParams) {
                                    if (!params.has(param) || params.getString(param).length() == 0) {
                                        throw new ApiException(ApiException.Type.MISSING_PARAMETER, param);
                                    }
                                }
                            }
                        }
                        res = impl.handleApiOptionView(name, params);
                        if (res == null) {
                            res = impl.handleApiView(name, params);
                        }
                        switch (format) {
                        case JSON:
                            response = res.toJSON().toString();
                            break;
                        case JSONP:
                            response = this.getJsonpWrapper(res.toJSON().toString());
                            break;
                        case XML:
                            response = this.responseToXml(name, res);
                            break;
                        case HTML:
                            response = this.responseToHtml(name, res);
                            break;
                        default:
                            break;
                        }

                        break;
                    case other:
                        ApiOther other = impl.getApiOther(name);
                        if (other != null) {
                            // Checking for null to handle option actions
                            if (!getOptionsParamApi().isDisableKey()
                                    && (!getOptionsParamApi().isNoKeyForSafeOps() || other.isRequiresApiKey())) {
                                // Check if a valid api key has been used
                                if (!this.hasValidKey(requestHeader, params)) {
                                    throw new ApiException(ApiException.Type.BAD_API_KEY);
                                }
                            }
                            List<String> mandatoryParams = other.getMandatoryParamNames();
                            if (mandatoryParams != null) {
                                for (String param : mandatoryParams) {
                                    if (!params.has(param) || params.getString(param).length() == 0) {
                                        throw new ApiException(ApiException.Type.MISSING_PARAMETER, param);
                                    }
                                }
                            }
                        }
                        msg = impl.handleApiOther(msg, name, params);
                        break;
                    case pconn:
                        ApiPersistentConnection pconn = impl.getApiPersistentConnection(name);
                        if (pconn != null) {
                            if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) {
                                if (!this.hasValidKey(requestHeader, params)) {
                                    throw new ApiException(ApiException.Type.BAD_API_KEY);
                                }
                            }
                            List<String> mandatoryParams = pconn.getMandatoryParamNames();
                            if (mandatoryParams != null) {
                                for (String param : mandatoryParams) {
                                    if (!params.has(param) || params.getString(param).length() == 0) {
                                        throw new ApiException(ApiException.Type.MISSING_PARAMETER, param);
                                    }
                                }
                            }
                        }
                        impl.handleApiPersistentConnection(msg, httpIn, httpOut, name, params);
                        return true;
                    }
                } else {
                    // Handle default front page, unless if the API UI is disabled
                    if (!isEnabled() || !getOptionsParamApi().isUiEnabled()) {
                        throw new ApiException(ApiException.Type.DISABLED);
                    }
                    response = webUI.handleRequest(requestHeader.getURI(), this.isEnabled());
                    format = Format.UI;
                    contentType = "text/html; charset=UTF-8";
                }
            }
            logger.debug("handleApiRequest returning: " + response);

        } catch (Exception e) {
            if (!getOptionsParamApi().isReportPermErrors()) {
                if (e instanceof ApiException) {
                    ApiException exception = (ApiException) e;
                    if (exception.getType().equals(ApiException.Type.DISABLED)
                            || exception.getType().equals(ApiException.Type.BAD_API_KEY)) {
                        // Fail silently
                        return true;
                    }
                }
            }
            handleException(msg, format, contentType, e);
            error = true;
        }

        if (!error && !format.equals(Format.OTHER) && shortcutImpl == null) {
            msg.setResponseHeader(getDefaultResponseHeader(contentType));
            msg.setResponseBody(response);
            msg.getResponseHeader().setContentLength(msg.getResponseBody().length());
        }

        if (impl != null) {
            impl.addCustomHeaders(name, reqType, msg);
        }

        httpOut.write(msg.getResponseHeader());
        httpOut.write(msg.getResponseBody().getBytes());
        httpOut.flush();
        httpOut.close();
        httpIn.close();

        return true;
    }

    /**
     * Returns a URI for the specified parameters.
     * <p>
     * An {@link #getOneTimeNonce(String) one time nonce query parameter} is added to the resulting URL, if required (that is,
     * not a view). In this case the URL is ended with an ampersand (for example,
     * {@code https://zap/format/prefix/action/name/?apinonce=xyz&}), otherwise it has a trailing slash (for example,
     * {@code http://zap/format/prefix/view/name/}).
     * 
     * @param format the format of the API response
     * @param prefix the prefix of the API implementor
     * @param type the request type
     * @param name the name of the endpoint
     * @param proxy if true then the URI returned will only work if proxying via ZAP, ie it will start with http://zap/..
     * @return the URL to access the defined endpoint
     * @see #getBaseURL(boolean)
     */
    public String getBaseURL(API.Format format, String prefix, API.RequestType type, String name, boolean proxy) {
        String apiPath = format.name() + "/" + prefix + "/" + type.name() + "/" + name + "/";
        if (!RequestType.view.equals(type)) {
            return getBaseURL(proxy) + apiPath + "?" + API_NONCE_PARAM + "=" + this.getOneTimeNonce("/" + apiPath)
                    + "&";
        }
        return getBaseURL(proxy) + apiPath;
    }

    /**
     * Gets the base URL to access the ZAP API, possibly proxying through ZAP.
     * <p>
     * If proxying through ZAP the base URL will use the custom domain, {@value #API_DOMAIN}.
     * <p>
     * The resulting base URL has a trailing slash, for example, {@code https://127.0.0.1/} or {@code https://zap/}.
     * 
     * @param proxy {@code true} if the URL will be accessed while proxying through ZAP, {@code false} otherwise.
     * @return the base URL to access the ZAP API.
     * @since 2.7.0
     */
    public String getBaseURL(boolean proxy) {
        if (proxy) {
            return getOptionsParamApi().isSecureOnly() ? API_URL_S : API_URL;
        }

        StringBuilder strBuilder = new StringBuilder(50);
        strBuilder.append("http");
        if (getOptionsParamApi().isSecureOnly()) {
            strBuilder.append('s');
        }
        strBuilder.append("://").append(getProxyParam().getProxyIp()).append(':')
                .append(getProxyParam().getProxyPort()).append('/');
        return strBuilder.toString();
    }

    private String responseToHtml(String name, ApiResponse res) {
        StringBuilder sb = new StringBuilder();
        sb.append("<head>\n");
        sb.append("</head>\n");
        sb.append("<body>\n");
        res.toHTML(sb);
        sb.append("</body>\n");
        return sb.toString();
    }

    private String responseToXml(String name, ApiResponse res) {
        try {
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

            Document doc = docBuilder.newDocument();
            Element rootElement = doc.createElement(name);
            doc.appendChild(rootElement);
            res.toXML(doc, rootElement);

            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            DOMSource source = new DOMSource(doc);

            StringWriter sw = new StringWriter();
            StreamResult result = new StreamResult(sw);
            transformer.transform(source, result);

            return sw.toString();

        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return "";
    }

    public static JSONObject getParams(String params) throws ApiException {
        JSONObject jp = new JSONObject();
        if (params == null || params.length() == 0) {
            return jp;
        }
        String[] keyValue = patternParam.split(params);
        String key = null;
        String value = null;
        int pos = 0;
        for (int i = 0; i < keyValue.length; i++) {
            key = null;
            pos = keyValue[i].indexOf('=');
            if (pos > 0) {
                // param found
                try {
                    key = URLDecoder.decode(keyValue[i].substring(0, pos), "UTF-8");
                    value = URLDecoder.decode(keyValue[i].substring(pos + 1), "UTF-8");
                    jp.put(key, value);
                } catch (UnsupportedEncodingException | IllegalArgumentException e) {
                    // Carry on anyway
                    Exception apiException = new ApiException(ApiException.Type.ILLEGAL_PARAMETER, params, e);
                    logger.error(apiException.getMessage(), apiException);
                }
            } else {
                // Carry on anyway
                Exception e = new ApiException(ApiException.Type.ILLEGAL_PARAMETER, params);
                logger.error(e.getMessage(), e);
            }
        }
        return jp;
    }

    private String getJsonpWrapper(String json) {
        return "zapJsonpResult (" + json + " )";
    }

    public Map<String, ApiImplementor> getImplementors() {
        return Collections.unmodifiableMap(implementors);
    }

    public String getCallBackUrl(ApiImplementor impl, String site) {
        String url = site + CALL_BACK_URL + random.nextLong();
        this.callBacks.put(url, impl);
        return url;
    }

    /**
     * Returns a one time nonce to be used with the API call specified by the URL
     * @param apiUrl the API URL
     * @return a one time nonce
     * @since 2.6.0
     */
    public String getOneTimeNonce(String apiUrl) {
        String nonce = Long.toHexString(random.nextLong());
        this.nonces.put(nonce, new Nonce(nonce, apiUrl, true));
        return nonce;
    }

    /**
     * Returns a nonce that will be valid for the lifetime of the ZAP process to used with the API call specified by the URL
     * @param apiUrl the API URL
     * @return a nonce that will be valid for the lifetime of the ZAP process
     * @since 2.6.0
     */
    public String getLongLivedNonce(String apiUrl) {
        String nonce = Long.toHexString(random.nextLong());
        this.nonces.put(nonce, new Nonce(nonce, apiUrl, false));
        return nonce;
    }

    /**
     * Returns true if the API call has a valid key
     * @param msg the message
     * @return true if the API call has a valid key
     * @since 2.6.0
     */
    public boolean hasValidKey(HttpMessage msg) {
        try {
            return this.hasValidKey(msg.getRequestHeader(),
                    getParams(msg.getRequestHeader().getURI().getEscapedQuery()));
        } catch (ApiException e) {
            logger.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * Returns true if the API call has a valid key
     * @param reqHeader the request header
     * @param params the parameters
     * @return true if the API call has a valid key
     * @since 2.6.0
     */
    public boolean hasValidKey(HttpRequestHeader reqHeader, JSONObject params) {
        try {
            String apiPath;
            try {
                apiPath = reqHeader.getURI().getPath();
            } catch (URIException e) {
                logger.error(e.getMessage(), e);
                return false;
            }
            String nonceParam = reqHeader.getHeader(HttpHeader.X_ZAP_API_NONCE);
            if (nonceParam == null && params.has(API_NONCE_PARAM)) {
                nonceParam = params.getString(API_NONCE_PARAM);
            }

            if (nonceParam != null) {
                Nonce nonce = nonces.get(nonceParam);
                if (nonce == null) {
                    logger.warn("API nonce " + nonceParam + " not found in request from "
                            + reqHeader.getSenderAddress().getHostAddress());
                    return false;
                } else if (nonce.isOneTime()) {
                    nonces.remove(nonceParam);
                }
                if (!nonce.isValid()) {
                    logger.warn("API nonce " + nonce.getNonceKey() + " expired at " + nonce.getExpires().toString()
                            + " in request from " + reqHeader.getSenderAddress().getHostAddress());
                    return false;
                }

                if (!apiPath.equals(nonce.getApiPath())) {
                    logger.warn("API nonce path was " + nonce.getApiPath() + " but call was for " + apiPath
                            + " in request from " + reqHeader.getSenderAddress().getHostAddress());
                    return false;
                }
            } else {
                String keyParam = reqHeader.getHeader(HttpHeader.X_ZAP_API_KEY);
                if (keyParam == null && params.has(API_KEY_PARAM)) {
                    keyParam = params.getString(API_KEY_PARAM);
                }
                if (!getOptionsParamApi().getKey().equals(keyParam)) {
                    logger.warn("API key incorrect or not supplied: " + keyParam + " in request from "
                            + reqHeader.getSenderAddress().getHostAddress());
                    return false;
                }
            }

            return true;
        } finally {
            synchronized (nonces) {
                for (Entry<String, Nonce> entry : nonces.entrySet()) {
                    if (!entry.getValue().isValid()) {
                        nonces.remove(entry.getKey());
                    }
                }
            }
        }
    }

    public static String getDefaultResponseHeader(String contentType) {
        return getDefaultResponseHeader(contentType, 0);
    }

    public static String getDefaultResponseHeader(String contentType, int contentLength) {
        return getDefaultResponseHeader(STATUS_OK, contentType, contentLength, false);
    }

    public static String getDefaultResponseHeader(String contentType, int contentLength, boolean canCache) {
        return getDefaultResponseHeader(STATUS_OK, contentType, contentLength, canCache);
    }

    public static String getDefaultResponseHeader(String responseStatus, String contentType, int contentLength) {
        return getDefaultResponseHeader(responseStatus, contentType, contentLength, false);
    }

    public static String getDefaultResponseHeader(String responseStatus, String contentType, int contentLength,
            boolean canCache) {
        StringBuilder sb = new StringBuilder(250);

        sb.append("HTTP/1.1 ").append(responseStatus).append("\r\n");
        if (!canCache) {
            sb.append("Pragma: no-cache\r\n");
            sb.append("Cache-Control: no-cache\r\n");
        }
        sb.append(
                "Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; child-src 'self'; img-src 'self' data:; font-src 'self' data:; style-src 'self'\r\n");
        sb.append("Referrer-Policy: no-referrer\r\n");
        sb.append("Access-Control-Allow-Methods: GET,POST,OPTIONS\r\n");
        sb.append("Access-Control-Allow-Headers: ZAP-Header\r\n");
        sb.append("X-Frame-Options: DENY\r\n");
        sb.append("X-XSS-Protection: 1; mode=block\r\n");
        sb.append("X-Content-Type-Options: nosniff\r\n");
        sb.append("X-Clacks-Overhead: GNU Terry Pratchett\r\n");
        sb.append("Content-Length: ").append(contentLength).append("\r\n");
        sb.append("Content-Type: ").append(contentType).append("\r\n");

        return sb.toString();
    }

    private void handleException(HttpMessage msg, Format format, String contentType, Exception cause) {
        String responseStatus = STATUS_INTERNAL_SERVER_ERROR;
        if (format == Format.OTHER) {
            boolean logError = true;
            if (cause instanceof ApiException) {
                switch (((ApiException) cause).getType()) {
                case DISABLED:
                case BAD_TYPE:
                case NO_IMPLEMENTOR:
                case BAD_API_KEY:
                case MISSING_PARAMETER:
                case BAD_ACTION:
                case BAD_VIEW:
                case BAD_OTHER:
                    responseStatus = STATUS_BAD_REQUEST;
                    logBadRequest(msg, cause);
                    logError = false;
                    break;
                default:
                }
            }

            if (logError) {
                logger.error("API 'other' endpoint didn't handle exception:", cause);
            }
        } else {
            ApiException exception;
            if (cause instanceof ApiException) {
                exception = (ApiException) cause;
                if (!ApiException.Type.INTERNAL_ERROR.equals(exception.getType())) {
                    responseStatus = STATUS_BAD_REQUEST;
                    logBadRequest(msg, cause);
                }
            } else {
                exception = new ApiException(ApiException.Type.INTERNAL_ERROR, cause);
                logger.error("Exception while handling API request:", cause);
            }
            String response = exception.toString(format, getOptionsParamApi().isIncErrorDetails());

            msg.getResponseBody().setCharset(getCharset(contentType));
            msg.getResponseBody().setBody(response);
        }

        try {
            msg.setResponseHeader(
                    getDefaultResponseHeader(responseStatus, contentType, msg.getResponseBody().length()));
        } catch (HttpMalformedHeaderException e) {
            logger.warn("Failed to build API error response:", e);
        }
    }

    private static void logBadRequest(HttpMessage msg, Exception cause) {
        logger.warn("Bad request to API endpoint [" + msg.getRequestHeader().getURI().getEscapedPath() + "] from ["
                + msg.getRequestHeader().getSenderAddress().getHostAddress() + "]:", cause);
    }

    private static String getCharset(String contentType) {
        int idx = contentType.indexOf("charset=");
        if (idx == -1) {
            return "UTF-8";
        }
        return contentType.substring(idx + 8);
    }

    private class Nonce {
        private final String nonceKey;
        private final String apiPath;
        private final boolean oneTime;
        private final Date expires;

        public Nonce(String nonceKey, String apiStr, boolean oneTime) {
            this.nonceKey = nonceKey;
            this.apiPath = apiStr;
            this.oneTime = oneTime;
            this.expires = DateUtils.addSeconds(new Date(), getOptionsParamApi().getNonceTimeToLiveInSecs());
        }

        public String getNonceKey() {
            return nonceKey;
        }

        public String getApiPath() {
            return apiPath;
        }

        public boolean isOneTime() {
            return oneTime;
        }

        public boolean isValid() {
            return !oneTime || expires.after(new Date());
        }

        public Date getExpires() {
            return expires;
        }

    }
}