/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2017 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.callback; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.httpclient.URIException; import org.apache.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.core.proxy.OverrideMessageProxyListener; import org.parosproxy.paros.core.proxy.ProxyServer; import org.parosproxy.paros.extension.ExtensionAdaptor; import org.parosproxy.paros.extension.ExtensionHook; import org.parosproxy.paros.extension.OptionsChangedListener; import org.parosproxy.paros.model.OptionsParam; import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.view.View; public class ExtensionCallback extends ExtensionAdaptor implements OptionsChangedListener { private static final String TEST_PREFIX = "ZapTest"; private ProxyServer proxyServer; private CallbackParam callbackParam; private OptionsCallbackPanel optionsCallbackPanel; private Map<String, CallbackImplementor> callbacks = new HashMap<String, CallbackImplementor>(); private int actualPort; private String currentConfigLocalAddress; private int currentConfigPort; private static final Logger LOGGER = Logger .getLogger(ExtensionCallback.class); public ExtensionCallback() { proxyServer = new ProxyServer("ZAP-CallbackServer"); proxyServer .addOverrideMessageProxyListener(new CallbackProxyListener()); } @Override public void hook(ExtensionHook extensionHook) { super.hook(extensionHook); extensionHook.addOptionsParamSet(getCallbackParam()); extensionHook.addOptionsChangedListener(this); if (View.isInitialised()) { extensionHook.getHookView().addOptionPanel( getOptionsCallbackPanel()); } } @Override public void optionsLoaded() { proxyServer.setConnectionParam(getModel().getOptionsParam() .getConnectionParam()); currentConfigLocalAddress = this.getCallbackParam().getLocalAddress(); currentConfigPort = this.getCallbackParam().getPort(); } @Override public void postInit() { this.restartServer(this.getCallbackParam().getPort()); } private void restartServer(int port) { // this will close the previous listener (if there was one) actualPort = proxyServer.startServer(this.getCallbackParam() .getLocalAddress(), port, true); LOGGER.info("Started callback server on " + this.getCallbackParam().getLocalAddress() + ":" + actualPort); } public String getCallbackAddress() { String addr = this.getCallbackParam().getRemoteAddress(); if (addr.contains(":")) { // Looks like its IPv6 return "http://[" + addr + "]:" + actualPort + "/"; } // Looks like IPv4 return "http://" + addr + ":" + actualPort + "/"; } public String getTestUrl() { return getCallbackAddress() + TEST_PREFIX; } protected int getPort() { return actualPort; } public void registerCallbackImplementor(CallbackImplementor impl) { for (String prefix : impl.getCallbackPrefixes()) { LOGGER.debug("Registering callback prefix: " + prefix); if (this.callbacks.containsKey(prefix)) { LOGGER.error("Duplicate callback prefix: " + prefix); } this.callbacks.put("/" + prefix, impl); } } public void removeCallbackImplementor(CallbackImplementor impl) { for (String shortcut : impl.getCallbackPrefixes()) { String key = "/" + shortcut; if (this.callbacks.containsKey(key)) { LOGGER.debug("Removing registered callback prefix: " + shortcut); this.callbacks.remove(key); } } } private CallbackParam getCallbackParam() { if (this.callbackParam == null) { this.callbackParam = new CallbackParam(); } return this.callbackParam; } private OptionsCallbackPanel getOptionsCallbackPanel() { if (optionsCallbackPanel == null) { optionsCallbackPanel = new OptionsCallbackPanel(this); } return optionsCallbackPanel; } @Override public String getAuthor() { return Constant.ZAP_TEAM; } @Override public String getDescription() { return Constant.messages.getString("callback.desc"); } @Override public void optionsChanged(OptionsParam optionsParam) { if (!currentConfigLocalAddress.equals(this.getCallbackParam() .getLocalAddress()) || currentConfigPort != this.getCallbackParam().getPort()) { // Somethings changed, reuse the port if its still a random one int port = actualPort; if (currentConfigPort != this.getCallbackParam().getPort()) { port = this.getCallbackParam().getPort(); } this.restartServer(port); // Save the new ones for next time currentConfigLocalAddress = this.getCallbackParam() .getLocalAddress(); currentConfigPort = this.getCallbackParam().getPort(); } } private class CallbackProxyListener implements OverrideMessageProxyListener { @Override public int getArrangeableListenerOrder() { return 0; } @Override public boolean onHttpRequestSend(HttpMessage msg) { try { String url = msg.getRequestHeader().getURI().toString(); String path = msg.getRequestHeader().getURI().getPath(); LOGGER.debug("Callback received for URL : " + url + " path : " + path + " from " + msg.getRequestHeader().getSenderAddress()); if (path.startsWith("/" + TEST_PREFIX)) { String str = Constant.messages.getString( "callback.test.msg", url, msg.getRequestHeader() .getSenderAddress().toString()); if (View.isInitialised()) { View.getSingleton().getOutputPanel() .appendAsync(str + "\n"); } LOGGER.info(str); return true; } else if (path.startsWith("/favicon.ico")) { // Just ignore - its automatically requested by browsers // e.g. when trying the test URL return true; } for (Entry<String, CallbackImplementor> callback : callbacks .entrySet()) { if (path.startsWith(callback.getKey())) { // Copy the message so that CallbackImplementors cant // return anything to the sender callback.getValue().handleCallBack(msg.cloneAll()); return true; } } LOGGER.error("No callback handler for URL : " + url + " from " + msg.getRequestHeader().getSenderAddress()); } catch (URIException e) { LOGGER.error(e.getMessage(), e); } return true; } @Override public boolean onHttpResponseReceived(HttpMessage msg) { return true; } } }