/* * 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. */ // ZAP: 2017/02/20 Issue 2699: Make SSLException handling more user friendly package org.parosproxy.paros.extension.manualrequest.http.impl; import java.awt.EventQueue; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import javax.net.ssl.SSLException; import javax.swing.ImageIcon; import javax.swing.JToggleButton; import org.apache.commons.httpclient.URI; import org.apache.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; import org.parosproxy.paros.control.Control.Mode; import org.parosproxy.paros.extension.history.ExtensionHistory; import org.parosproxy.paros.extension.manualrequest.MessageSender; import org.parosproxy.paros.model.HistoryReference; import org.parosproxy.paros.model.Model; import org.parosproxy.paros.model.Session; import org.parosproxy.paros.network.HttpMalformedHeaderException; import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.network.HttpSender; import org.parosproxy.paros.view.View; import org.zaproxy.zap.PersistentConnectionListener; import org.zaproxy.zap.ZapGetMethod; import org.zaproxy.zap.extension.httppanel.HttpPanel; import org.zaproxy.zap.extension.httppanel.HttpPanelRequest; import org.zaproxy.zap.extension.httppanel.HttpPanelResponse; import org.zaproxy.zap.extension.httppanel.Message; import org.zaproxy.zap.model.SessionStructure; import org.zaproxy.zap.network.HttpRedirectionValidator; import org.zaproxy.zap.network.HttpRequestConfig; /** * Knows how to send {@link HttpMessage} objects. */ public class HttpPanelSender implements MessageSender { private static final Logger logger = Logger .getLogger(HttpPanelSender.class); private final HttpPanelResponse responsePanel; private ExtensionHistory extension; private HttpSender delegate; private JToggleButton followRedirect = null; private JToggleButton useTrackingSessionState = null; private List<PersistentConnectionListener> persistentConnectionListener = new ArrayList<>(); public HttpPanelSender(HttpPanelRequest requestPanel, HttpPanelResponse responsePanel) { this.responsePanel = responsePanel; requestPanel.addOptions(getButtonUseTrackingSessionState(), HttpPanel.OptionsLocation.AFTER_COMPONENTS); requestPanel.addOptions(getButtonFollowRedirects(), HttpPanel.OptionsLocation.AFTER_COMPONENTS); final boolean isSessionTrackingEnabled = Model.getSingleton() .getOptionsParam().getConnectionParam().isHttpStateEnabled(); getButtonUseTrackingSessionState().setEnabled(isSessionTrackingEnabled); } @Override public void handleSendMessage(Message aMessage) throws IllegalArgumentException, IOException { final HttpMessage httpMessage = (HttpMessage) aMessage; try { final ModeRedirectionValidator redirectionValidator = new ModeRedirectionValidator(); if (getButtonFollowRedirects().isSelected()) { getDelegate().sendAndReceive( httpMessage, HttpRequestConfig.builder().setRedirectionValidator(redirectionValidator).build()); } else { getDelegate().sendAndReceive(httpMessage, false); } EventQueue.invokeAndWait(new Runnable() { @Override public void run() { if (!httpMessage.getResponseHeader().isEmpty()) { // Indicate UI new response arrived responsePanel.updateContent(); try { Session session = Model.getSingleton().getSession(); HistoryReference ref = new HistoryReference(session, HistoryReference.TYPE_ZAP_USER, httpMessage); final ExtensionHistory extHistory = getHistoryExtension(); if (extHistory != null) { extHistory.addHistory(ref); } SessionStructure.addPath(session, ref, httpMessage); } catch (final Exception e) { logger.error(e.getMessage(), e); } if (!redirectionValidator.isRequestValid()) { View.getSingleton().showWarningDialog( Constant.messages.getString( "manReq.outofscope.redirection.warning", redirectionValidator.getInvalidRedirection())); } } } }); ZapGetMethod method = (ZapGetMethod) httpMessage.getUserObject(); notifyPersistentConnectionListener(httpMessage, null, method); } catch (final HttpMalformedHeaderException mhe) { throw new IllegalArgumentException("Malformed header error.", mhe); } catch (final UnknownHostException uhe) { throw new IOException("Error forwarding to an Unknown host: " + uhe.getMessage(), uhe); } catch (final SSLException sslEx) { throw sslEx; } catch (final IOException ioe) { throw new IOException("IO error in sending request: " + ioe.getClass() + ": " + ioe.getMessage(), ioe); } catch (final Exception e) { logger.error(e.getMessage(), e); } } /** * Go thru each listener and offer him to take over the connection. The * first observer that returns true gets exclusive rights. * * @param httpMessage Contains HTTP request & response. * @param inSocket Encapsulates the TCP connection to the browser. * @param method Provides more power to process response. * * @return Boolean to indicate if socket should be kept open. */ private boolean notifyPersistentConnectionListener(HttpMessage httpMessage, Socket inSocket, ZapGetMethod method) { boolean keepSocketOpen = false; PersistentConnectionListener listener = null; synchronized (persistentConnectionListener) { for (int i = 0; i < persistentConnectionListener.size(); i++) { listener = persistentConnectionListener.get(i); try { if (listener.onHandshakeResponse(httpMessage, inSocket, method)) { // inform as long as one listener wishes to overtake the connection keepSocketOpen = true; break; } } catch (Exception e) { logger.warn(e.getMessage(), e); } } } return keepSocketOpen; } protected ExtensionHistory getHistoryExtension() { if (extension == null) { extension = ((ExtensionHistory) Control.getSingleton() .getExtensionLoader().getExtension(ExtensionHistory.NAME)); } return extension; } @Override public void cleanup() { if (delegate != null) { delegate.shutdown(); delegate = null; } final boolean isSessionTrackingEnabled = Model.getSingleton() .getOptionsParam().getConnectionParam().isHttpStateEnabled(); getButtonUseTrackingSessionState().setEnabled(isSessionTrackingEnabled); } private HttpSender getDelegate() { if (delegate == null) { delegate = new HttpSender(Model.getSingleton().getOptionsParam() .getConnectionParam(), getButtonUseTrackingSessionState() .isSelected(), HttpSender.MANUAL_REQUEST_INITIATOR); } return delegate; } private JToggleButton getButtonFollowRedirects() { if (followRedirect == null) { followRedirect = new JToggleButton(new ImageIcon( HttpPanelSender.class .getResource("/resource/icon/16/118.png"))); // Arrow // turn // around // left followRedirect.setToolTipText(Constant.messages .getString("manReq.checkBox.followRedirect")); followRedirect.setSelected(true); } return followRedirect; } private JToggleButton getButtonUseTrackingSessionState() { if (useTrackingSessionState == null) { useTrackingSessionState = new JToggleButton(new ImageIcon( HttpPanelSender.class .getResource("/resource/icon/fugue/cookie.png"))); // Cookie useTrackingSessionState.setToolTipText(Constant.messages .getString("manReq.checkBox.useSession")); } return useTrackingSessionState; } public void addPersistentConnectionListener(PersistentConnectionListener listener) { persistentConnectionListener.add(listener); } public void removePersistentConnectionListener(PersistentConnectionListener listener) { persistentConnectionListener.remove(listener); } /** * A {@link HttpRedirectionValidator} that enforces the {@link Mode} when validating the {@code URI} of redirections. * * @see #isRequestValid() */ private static class ModeRedirectionValidator implements HttpRedirectionValidator { private boolean isRequestValid; private URI invalidRedirection; public ModeRedirectionValidator() { isRequestValid = true; } @Override public void notifyMessageReceived(HttpMessage message) { // Nothing to do with the message. } @Override public boolean isValid(URI redirection) { if (!isValidForCurrentMode(redirection)) { isRequestValid = false; invalidRedirection = redirection; return false; } return true; } private boolean isValidForCurrentMode(URI uri) { switch (Control.getSingleton().getMode()) { case safe: return false; case protect: return Model.getSingleton().getSession().isInScope(uri.toString()); default: return true; } } /** * Tells whether or not the request is valid, that is, all redirections were valid for the current {@link Mode}. * * @return {@code true} is the request is valid, {@code false} otherwise. * @see #getInvalidRedirection() */ public boolean isRequestValid() { return isRequestValid; } /** * Gets the invalid redirection, if any. * * @return the invalid redirection, {@code null} if there was none. * @see #isRequestValid() */ public URI getInvalidRedirection() { return invalidRedirection; } } }