Package org.zaproxy.zap.extension.websocket

Source Code of org.zaproxy.zap.extension.websocket.ExtensionWebSocket

/*
* 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.websocket;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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.db.Database;
import org.parosproxy.paros.db.RecordSessionUrl;
import org.parosproxy.paros.extension.ExtensionAdaptor;
import org.parosproxy.paros.extension.ExtensionHook;
import org.parosproxy.paros.extension.ExtensionHookMenu;
import org.parosproxy.paros.extension.ExtensionHookView;
import org.parosproxy.paros.extension.ExtensionLoader;
import org.parosproxy.paros.extension.SessionChangedListener;
import org.parosproxy.paros.extension.filter.ExtensionFilter;
import org.parosproxy.paros.extension.manualrequest.ExtensionManualRequestEditor;
import org.parosproxy.paros.extension.manualrequest.ManualRequestEditorDialog;
import org.parosproxy.paros.extension.manualrequest.http.impl.ManualHttpRequestEditorDialog;
import org.parosproxy.paros.model.HistoryReference;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.model.Session;
import org.parosproxy.paros.model.SiteNode;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.view.AbstractParamPanel;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.PersistentConnectionListener;
import org.zaproxy.zap.ZapGetMethod;
import org.zaproxy.zap.extension.brk.BreakpointMessageHandler;
import org.zaproxy.zap.extension.brk.ExtensionBreak;
import org.zaproxy.zap.extension.fuzz.ExtensionFuzz;
import org.zaproxy.zap.extension.help.ExtensionHelp;
import org.zaproxy.zap.extension.httppanel.Message;
import org.zaproxy.zap.extension.httppanel.component.HttpPanelComponentInterface;
import org.zaproxy.zap.extension.httppanel.view.HttpPanelDefaultViewSelector;
import org.zaproxy.zap.extension.httppanel.view.HttpPanelView;
import org.zaproxy.zap.extension.httppanel.view.hex.HttpPanelHexView;
import org.zaproxy.zap.extension.websocket.brk.PopupMenuAddBreakWebSocket;
import org.zaproxy.zap.extension.websocket.brk.WebSocketBreakpointMessageHandler;
import org.zaproxy.zap.extension.websocket.brk.WebSocketBreakpointsUiManagerInterface;
import org.zaproxy.zap.extension.websocket.brk.WebSocketProxyListenerBreak;
import org.zaproxy.zap.extension.websocket.db.TableWebSocket;
import org.zaproxy.zap.extension.websocket.db.WebSocketStorage;
import org.zaproxy.zap.extension.websocket.filter.FilterWebSocketPayload;
import org.zaproxy.zap.extension.websocket.filter.WebSocketFilter;
import org.zaproxy.zap.extension.websocket.filter.WebSocketFilterListener;
import org.zaproxy.zap.extension.websocket.fuzz.ShowFuzzMessageInWebSocketsTabMenuItem;
import org.zaproxy.zap.extension.websocket.fuzz.WebSocketFuzzerHandler;
import org.zaproxy.zap.extension.websocket.manualsend.ManualWebSocketSendEditorDialog;
import org.zaproxy.zap.extension.websocket.manualsend.WebSocketPanelSender;
import org.zaproxy.zap.extension.websocket.ui.ExcludeFromWebSocketsMenuItem;
import org.zaproxy.zap.extension.websocket.ui.OptionsParamWebSocket;
import org.zaproxy.zap.extension.websocket.ui.OptionsWebSocketPanel;
import org.zaproxy.zap.extension.websocket.ui.PopupExcludeWebSocketContextMenu;
import org.zaproxy.zap.extension.websocket.ui.PopupIncludeWebSocketContextMenu;
import org.zaproxy.zap.extension.websocket.ui.ResendWebSocketMessageMenuItem;
import org.zaproxy.zap.extension.websocket.ui.SessionExcludeFromWebSocket;
import org.zaproxy.zap.extension.websocket.ui.WebSocketPanel;
import org.zaproxy.zap.extension.websocket.ui.httppanel.component.WebSocketComponent;
import org.zaproxy.zap.extension.websocket.ui.httppanel.models.ByteWebSocketPanelViewModel;
import org.zaproxy.zap.extension.websocket.ui.httppanel.models.StringWebSocketPanelViewModel;
import org.zaproxy.zap.extension.websocket.ui.httppanel.views.WebSocketSyntaxHighlightTextView;
import org.zaproxy.zap.extension.websocket.ui.httppanel.views.large.WebSocketLargePayloadUtil;
import org.zaproxy.zap.extension.websocket.ui.httppanel.views.large.WebSocketLargePayloadView;
import org.zaproxy.zap.extension.websocket.ui.httppanel.views.large.WebSocketLargetPayloadViewModel;
import org.zaproxy.zap.view.HttpPanelManager;
import org.zaproxy.zap.view.HttpPanelManager.HttpPanelComponentFactory;
import org.zaproxy.zap.view.HttpPanelManager.HttpPanelDefaultViewSelectorFactory;
import org.zaproxy.zap.view.HttpPanelManager.HttpPanelViewFactory;
import org.zaproxy.zap.view.SiteMapListener;
import org.zaproxy.zap.view.SiteMapTreeCellRenderer;
/**
* The WebSockets-extension takes over after the HTTP based WebSockets handshake
* is finished.
*
* @author Robert Koch
*/
public class ExtensionWebSocket extends ExtensionAdaptor implements
    PersistentConnectionListener, SessionChangedListener, SiteMapListener {
   
  private static final Logger logger = Logger.getLogger(ExtensionWebSocket.class);
 
  public static final int HANDSHAKE_LISTENER = 10;
 
  /**
   * Name of this extension.
   */
  public static final String NAME = "ExtensionWebSocket";

  /**
   * Used to shorten the time, a listener is started on a WebSocket channel.
   */
  private ExecutorService listenerThreadPool;

  /**
   * List of observers where each element is informed on all channel's
   * messages.
   */
  private Vector<WebSocketObserver> allChannelObservers;

  /**
   * Contains all proxies with their corresponding handshake message.
   */
  private Map<Integer, WebSocketProxy> wsProxies;

  /**
   * Interface to database.
   */
  private WebSocketStorage storage;

  /**
   * List of WebSocket related filters.
   */
  private WebSocketFilterListener wsFilterListener;

  /**
   * Different options in config.xml can change this extension's behavior.
   */
  private OptionsParamWebSocket config;

  /**
   * Current mode of ZAP. Determines if "unsafe" actions are allowed.
   */
  private Mode mode;

  /**
   * Link to {@link ExtensionFuzz}.
   */
  private WebSocketFuzzerHandler fuzzHandler;

  /**
   * Messages for some {@link WebSocketProxy} on this list are just
   * forwarded, but not stored nor shown in UI.
   */
  private List<Pattern> preparedIgnoredChannels;

  /**
   * Contains raw regex values, as they appear in the sessions dialogue.
   */
  private List<String> ignoredChannelList;

  /**
   * This filter allows to change the bytes when passed through ZAP.
   */
  private WebSocketFilter payloadFilter;
 
  public ExtensionWebSocket() {
    super(NAME);
   
    // should be initialized after ExtensionBreak (24) and
    // ExtensionFilter (8) and ManualRequestEditor (36)
    setOrder(150);
  }
 
  @Override
  public void init() {
    super.init();
   
    allChannelObservers = new Vector<>();
    wsProxies = new HashMap<>();
    config = new OptionsParamWebSocket();
   
    // setup database
    storage = new WebSocketStorage(createTableWebSocket());
    addAllChannelObserver(storage);
   
    preparedIgnoredChannels = new ArrayList<>();
    ignoredChannelList = new ArrayList<>();
  }
 
  private TableWebSocket createTableWebSocket() {
    TableWebSocket table = new TableWebSocket();
    Database db = Model.getSingleton().getDb();
    db.addDatabaseListener(table);
    try {
      table.databaseOpen(db.getDatabaseServer());
    } catch (SQLException e) {
      logger.warn(e.getMessage(), e);
    }
    return table;
  }

  /**
   * This method interweaves the WebSocket extension with the rest of ZAP.
   * <p>
   * It does the following things:
   * <ul>
   * <li>listens to new WebSocket connections</li>
   * <li>installs itself as session listener in order to react on session
   * changes</li>
   * <li>adds a WebSocket tab to the status panel (information window containing
   * e.g.: the History tab)</li>
   * <li>adds a WebSocket specific options panel</li>
   * <li>sets up context menu for WebSockets panel with 'Break' & 'Exclude'</li>
   * </ul>
   * </p>
   */
  @Override
  public void hook(ExtensionHook extensionHook) {
    super.hook(extensionHook);
   
    extensionHook.addPersistentConnectionListener(this);
   
    extensionHook.addSessionListener(this);
   
    extensionHook.addSiteMapListener(this);

    // setup configuration
    extensionHook.addOptionsParamSet(config);
   
    try {
      setChannelIgnoreList(Model.getSingleton().getSession().getExcludeFromProxyRegexs());
    } catch (WebSocketException e) {
      logger.warn(e.getMessage(), e);
    }
   
    if (getView() != null) {
      ExtensionLoader extLoader = Control.getSingleton().getExtensionLoader();
      ExtensionHookView hookView = extensionHook.getHookView();
      ExtensionHookMenu hookMenu = extensionHook.getHookMenu();
     
      // setup WebSocket tab
      WebSocketPanel wsPanel = getWebSocketPanel();
      wsPanel.setDisplayPanel(getView().getRequestPanel(), getView().getResponsePanel());
     
      extensionHook.addSessionListener(wsPanel.getSessionListener());
     
      addAllChannelObserver(wsPanel);
      ExtensionHelp.enableHelpKey(wsPanel, "websocket.tab");
     
      hookView.addStatusPanel(getWebSocketPanel());
     
      // setup Options Panel
      hookView.addOptionPanel(getOptionsPanel());
     
      // add 'Exclude from WebSockets' menu item to WebSocket tab context menu
      hookMenu.addPopupMenuItem(new ExcludeFromWebSocketsMenuItem(this, storage.getTable()));

      // setup Session Properties
      sessionExcludePanel =  new SessionExcludeFromWebSocket(this);
      getView().getSessionDialog().addParamPanel(new String[]{}, sessionExcludePanel, false);
     
      // setup Breakpoints
      ExtensionBreak extBreak = (ExtensionBreak) extLoader.getExtension(ExtensionBreak.NAME);
      if (extBreak != null) {
        // setup custom breakpoint handler
        BreakpointMessageHandler wsBrkMessageHandler = new WebSocketBreakpointMessageHandler(extBreak.getBreakPanel(), config);
        wsBrkMessageHandler.setEnabledBreakpoints(extBreak.getBreakpointsEnabledList());
       
        // listen on new messages such that breakpoints can apply
        addAllChannelObserver(new WebSocketProxyListenerBreak(this, wsBrkMessageHandler));

        // pop up to add the breakpoint
        hookMenu.addPopupMenuItem(new PopupMenuAddBreakWebSocket(extBreak));
        extBreak.addBreakpointsUiManager(getBrkManager());
      }
     
      // setup replace payload filter
      wsFilterListener = new WebSocketFilterListener();
      addAllChannelObserver(wsFilterListener);
      payloadFilter = new FilterWebSocketPayload(this, wsPanel.getChannelsModel());
      addWebSocketFilter(payloadFilter);
     
      // setup fuzzable extension
      ExtensionFuzz extFuzz = (ExtensionFuzz) extLoader.getExtension(ExtensionFuzz.NAME);
      if (extFuzz != null) {
        hookMenu.addPopupMenuItem(new ShowFuzzMessageInWebSocketsTabMenuItem(getWebSocketPanel()));
       
        fuzzHandler = new WebSocketFuzzerHandler(storage.getTable());
        extFuzz.addFuzzerHandler(WebSocketMessageDTO.class, fuzzHandler);
        addAllChannelObserver(fuzzHandler);
      }
     
      // add exclude/include scope
      hookMenu.addPopupMenuItem(new PopupIncludeWebSocketContextMenu());
      hookMenu.addPopupMenuItem(new PopupExcludeWebSocketContextMenu());
     
      // setup workpanel (window containing Request, Response & Break tab)
      initializeWebSocketsForWorkPanel();
     
      // setup manualrequest extension
      ExtensionManualRequestEditor extManReqEdit = (ExtensionManualRequestEditor) extLoader
          .getExtension(ExtensionManualRequestEditor.NAME);
      if (extManReqEdit != null) {
        WebSocketPanelSender sender = new WebSocketPanelSender();
        addAllChannelObserver(sender);
       
        sendDialog = createManualSendDialog(sender);
        extManReqEdit.addManualSendEditor(sendDialog);
        hookMenu.addToolsMenuItem(sendDialog.getMenuItem());
       
        // add 'Resend Message' menu item to WebSocket tab context menu
        hookMenu.addPopupMenuItem(new ResendWebSocketMessageMenuItem(createReSendDialog(sender)));
       
       
        // setup persistent connection listener for http manual send editor
        ManualRequestEditorDialog sendEditor = extManReqEdit.getManualSendEditor(HttpMessage.class);
        if (sendEditor != null) {
          ManualHttpRequestEditorDialog httpSendEditor = (ManualHttpRequestEditorDialog) sendEditor;
          httpSendEditor.addPersistentConnectionListener(this);
        }
      }
    }
  }
 
  @Override
  public boolean canUnload() {
    return true;
  }
 
  @Override
  public void unload() {
    super.unload();
   
    // close all existing connections
    for (Entry<Integer, WebSocketProxy> wsEntry : wsProxies.entrySet()) {
      WebSocketProxy wsProxy = wsEntry.getValue();
      wsProxy.shutdown();
    }
   
    Control control = Control.getSingleton();
    ExtensionLoader extLoader = control.getExtensionLoader();
   
    // clear up Session Properties
    getView().getSessionDialog().removeParamPanel(sessionExcludePanel);
   
    // clear up Breakpoints
    ExtensionBreak extBreak = (ExtensionBreak) extLoader.getExtension(ExtensionBreak.NAME);
    if (extBreak != null) {
      extBreak.removeBreakpointsUiManager(getBrkManager());
    }
   
    // clear up fuzzable extension
    ExtensionFuzz extFuzz = (ExtensionFuzz) extLoader.getExtension(ExtensionFuzz.NAME);
    if (extFuzz != null) {
      extFuzz.removeFuzzerHandler(WebSocketMessageDTO.class);
    }
   
    removeWebSocketFilter(payloadFilter);
   
    // clear up manualrequest extension
    ExtensionManualRequestEditor extManReqEdit = (ExtensionManualRequestEditor) extLoader
        .getExtension(ExtensionManualRequestEditor.NAME);
    if (extManReqEdit != null) {
      extManReqEdit.removeManualSendEditor(WebSocketMessageDTO.class);
     
      // clear up persistent connection listener for http manual send editor
      ManualRequestEditorDialog sendEditor = extManReqEdit.getManualSendEditor(HttpMessage.class);
      if (sendEditor != null) {
        ManualHttpRequestEditorDialog httpSendEditor = (ManualHttpRequestEditorDialog) sendEditor;
        httpSendEditor.removePersistentConnectionListener(this);
      }
    }
   
    if (getView() != null) {
      clearupWebSocketsForWorkPanel();
    }
  }

  @Override
  public String getAuthor() {
    return Constant.ZAP_TEAM;
  }
 
  @Override
  public String getDescription() {
    return Constant.messages.getString("websocket.desc");
  }

  /**
   * Add an observer that is attached to every channel connected in future.
   *
   * @param observer
   */
  public void addAllChannelObserver(WebSocketObserver observer) {
    allChannelObservers.add(observer);
  }

  /**
   * Add another WebSocket specific filter instance. Listens also to normal
   * HTTP communication.
   *
   * @param filter Instance receives payloads and is able to change it.
   */
  public void addWebSocketFilter(WebSocketFilter filter) {
    ExtensionLoader extLoader = Control.getSingleton().getExtensionLoader();
    ExtensionFilter extFilter = (ExtensionFilter) extLoader.getExtension(ExtensionFilter.NAME);
    if (extFilter != null) {
      filter.initView(getView());
      extFilter.addFilter(filter);
     
      wsFilterListener.addFilter(filter);
    } else {
      logger.warn("Filter '" + filter.getClass().toString() + "' couldn't be added as the filter extension is not available!");
    }
  }
 
  public void removeWebSocketFilter(WebSocketFilter filter) {
    ExtensionLoader extLoader = Control.getSingleton().getExtensionLoader();
    ExtensionFilter extFilter = (ExtensionFilter) extLoader.getExtension(ExtensionFilter.NAME);
    if (extFilter != null) {
      extFilter.removeFilter(filter);
      wsFilterListener.removeFilter(filter);
    }
  }

  @Override
  public int getArrangeableListenerOrder() {
    return HANDSHAKE_LISTENER;
  }

  @Override
  public boolean onHandshakeResponse(HttpMessage httpMessage, Socket inSocket, ZapGetMethod method) {
    boolean keepSocketOpen = false;
   
    if (httpMessage.isWebSocketUpgrade()) {
      logger.debug("Got WebSockets upgrade request. Handle socket connection over to WebSockets extension.");
      if (View.isInitialised()) {
        // Show the tab in case its been closed
        this.getWebSocketPanel().setTabFocus();
      }
     
      if (method != null) {
        Socket outSocket = method.getUpgradedConnection();
        InputStream outReader = method.getUpgradedInputStream();
       
        keepSocketOpen = true;
       
        addWebSocketsChannel(httpMessage, inSocket, outSocket, outReader);
      } else {
        logger.error("Unable to retrieve upgraded outgoing channel.");
      }
    }
   
    return keepSocketOpen;
  }

  /**
   * Add an open channel to this extension after
   * HTTP handshake has been completed.
   *
   * @param handshakeMessage HTTP-based handshake.
   * @param localSocket Current connection channel from the browser to ZAP.
   * @param remoteSocket Current connection channel from ZAP to the server.
   * @param remoteReader Current {@link InputStream} of remote connection.
   */
  public void addWebSocketsChannel(HttpMessage handshakeMessage, Socket localSocket, Socket remoteSocket, InputStream remoteReader) {
    try {     
      if (logger.isDebugEnabled()) {
        String source = (localSocket != null) ? localSocket.getInetAddress().toString() + ":" + localSocket.getPort() : "ZAP";
        String destination = remoteSocket.getInetAddress() + ":" + remoteSocket.getPort();
       
        logger.debug("Got WebSockets channel from " + source + " to " + destination);
      }
     
      // parse HTTP handshake
      Map<String, String> wsExtensions = parseWebSocketExtensions(handshakeMessage);
      String wsProtocol = parseWebSocketSubProtocol(handshakeMessage);
      String wsVersion = parseWebSocketVersion(handshakeMessage);
 
      WebSocketProxy wsProxy = null;
      wsProxy = WebSocketProxy.create(wsVersion, localSocket, remoteSocket, wsProtocol, wsExtensions);
     
      // set other observers and handshake reference, before starting listeners
      for (WebSocketObserver observer : allChannelObservers) {
        wsProxy.addObserver(observer);
      }
     
      // wait until HistoryReference is saved to database
      while (handshakeMessage.getHistoryRef() == null) {
        try {
          Thread.sleep(5);
        } catch (InterruptedException e) {
          logger.warn(e.getMessage(), e);
        }
      }
      wsProxy.setHandshakeReference(handshakeMessage.getHistoryRef());
      wsProxy.setForwardOnly(isChannelIgnored(wsProxy.getDTO()));
      wsProxy.startListeners(getListenerThreadPool(), remoteReader);
     
      synchronized (wsProxies) {
        wsProxies.put(wsProxy.getChannelId(), wsProxy);
      }
    } catch (Exception e) {
      // defensive measure to catch all possible exceptions
      // cleanly close resources
      if (localSocket != null && !localSocket.isClosed()) {
        try {
          localSocket.close();
        } catch (IOException e1) {
          logger.warn(e.getMessage(), e1);
        }
      }
     
      if (remoteReader != null) {
        try {
          remoteReader.close();
        } catch (IOException e1) {
          logger.warn(e.getMessage(), e1);
        }
      }
     
      if (remoteSocket != null && !remoteSocket.isClosed()) {
        try {
          remoteSocket.close();
        } catch (IOException e1) {
          logger.warn(e.getMessage(), e1);
        }
      }
      logger.error("Adding WebSockets channel failed due to: '" + e.getClass() + "' " + e.getMessage());
      return;
    }
  }

  /**
   * Parses the negotiated WebSockets extensions. It splits them up into name
   * and params of the extension. In future we want to look up if given
   * extension is available as ZAP extension and then use their knowledge to
   * process frames.
   * <p>
   * If multiple extensions are to be used, they can all be listed in a single
   * {@link WebSocketProtocol#HEADER_EXTENSION} field or split between multiple
   * instances of the {@link WebSocketProtocol#HEADER_EXTENSION} header field.
   *
   * @param msg
   * @return Map with extension name and parameter string.
   */
  private Map<String, String> parseWebSocketExtensions(HttpMessage msg) {
    Vector<String> extensionHeaders = msg.getResponseHeader().getHeaders(
        WebSocketProtocol.HEADER_EXTENSION);

    if (extensionHeaders == null) {
      return null;
    }
   
    /*
     * From http://tools.ietf.org/html/rfc6455#section-4.3:
     *   extension-list = 1#extension
         *   extension = extension-token *( ";" extension-param )
         *   extension-token = registered-token
         *   registered-token = token
         *   extension-param = token [ "=" (token | quoted-string) ]
         *    ; When using the quoted-string syntax variant, the value
         *    ; after quoted-string unescaping MUST conform to the
         *    ; 'token' ABNF.
         *   
         * e.g.:    Sec-WebSocket-Extensions: foo
         *       Sec-WebSocket-Extensions: bar; baz=2
     *      is exactly equivalent to:
     *       Sec-WebSocket-Extensions: foo, bar; baz=2
     *
     * e.g.:  Sec-WebSocket-Extensions: deflate-stream
     *       Sec-WebSocket-Extensions: mux; max-channels=4; flow-control, deflate-stream
     *       Sec-WebSocket-Extensions: private-extension
     */
    Map<String, String> wsExtensions = new LinkedHashMap<>();
    for (String extensionHeader : extensionHeaders) {
      for (String extension : extensionHeader.split(",")) {
        String key = extension.trim();
        String params = "";
       
        int paramsIndex = key.indexOf(";");
        if (paramsIndex != -1) {
          key = extension.substring(0, paramsIndex).trim();
          params = extension.substring(paramsIndex + 1).trim();
        }
       
        wsExtensions.put(key, params);
      }
    }
   
    /*
     * The interpretation of any extension parameters, and what constitutes
     * a valid response by a server to a requested set of parameters by a
     * client, will be defined by each such extension.
     *
     * Note that the order of extensions is significant!
     */
   
    return wsExtensions;
  }

  /**
   * Parses negotiated protocols out of the response header.
   * <p>
   * The {@link WebSocketProtocol#HEADER_PROTOCOL} header is only allowed to
   * appear once in the HTTP response (but several times in the HTTP request).
   *
   * A server that speaks multiple sub-protocols has to make sure it selects
   * one based on the client's handshake and specifies it in its handshake.
   *
   * @param msg
   * @return Name of negotiated sub-protocol or null.
   */
  private String parseWebSocketSubProtocol(HttpMessage msg) {
    String subProtocol = msg.getResponseHeader().getHeader(
        WebSocketProtocol.HEADER_PROTOCOL);
    return subProtocol;
  }

  /**
   * The {@link WebSocketProtocol#HEADER_VERSION} header might not always
   * contain a number. Therefore I return a string. Use the version to choose
   * the appropriate processing class.
   *
   * @param msg
   * @return Version of the WebSockets channel, defining the protocol.
   */
  private String parseWebSocketVersion(HttpMessage msg) {
    String version = msg.getResponseHeader().getHeader(
        WebSocketProtocol.HEADER_VERSION);
   
    if (version == null) {
      // check for requested WebSockets version
      version = msg.getRequestHeader().getHeader(WebSocketProtocol.HEADER_VERSION);
     
      if (version == null) {
        // default to version 13 if non is given, for whatever reason
        logger.debug("No " + WebSocketProtocol.HEADER_VERSION + " header was provided - try version 13");
        version = "13";
      }
    }
   
    return version;
  }

  /**
   * Creates and returns a cached thread pool that should speed up
   * {@link WebSocketListener}.
   *
   * @return
   */
  private ExecutorService getListenerThreadPool() {
    if (listenerThreadPool == null) {
      listenerThreadPool = Executors.newCachedThreadPool();
    }
    return listenerThreadPool;
  }

  /**
   * Returns true if the WebSocket connection that followed the given
   * WebSocket handshake is already alive.
   *
   * @param handshakeRef
   * @return True if connection is still alive.
   */
  public boolean isConnected(HistoryReference handshakeRef) {
    int historyId = handshakeRef.getHistoryId();
    synchronized (wsProxies) {
      for (Entry<Integer, WebSocketProxy> entry : wsProxies.entrySet()) {
        WebSocketProxy proxy = entry.getValue();
        if (historyId == proxy.getHandshakeReference().getHistoryId()) {
          return proxy.isConnected();
        }
      }
    }
    return false;
  }

  /**
   * Returns true if given channel id is connected.
   *
   * @param channelId
   * @return True if connection is still alive.
   */
  public boolean isConnected(Integer channelId) {
    synchronized (wsProxies) {
      if (wsProxies.containsKey(channelId)) {
        return wsProxies.get(channelId).isConnected();
      }
    }
    return false;
  }

    /**
   * Submitted list of strings will be interpreted as regular expression on
   * WebSocket channel URLs.
   * <p>
   * While connections to those excluded URLs will be established and messages
   * will be forwarded, nothing is stored nor can you view the communication
   * in the UI.
   *
   * @param ignoreList
     * @throws WebSocketException
   */
  public void setChannelIgnoreList(List<String> ignoreList) throws WebSocketException {
    preparedIgnoredChannels.clear();
   
    List<String> nonEmptyIgnoreList = new ArrayList<>();
    for (String regex : ignoreList) {
      if (regex.trim().length() > 0) {
        nonEmptyIgnoreList.add(regex);
      }
    }

    // ensure validity by compiling regular expression
    // store them for better performance
    for (String regex : nonEmptyIgnoreList) {
      if (regex.trim().length() > 0) {
        preparedIgnoredChannels.add(Pattern.compile(regex.trim(), Pattern.CASE_INSENSITIVE));
      }
    }
   
    // save list in database
    try {
      Model.getSingleton().getDb().getTableSessionUrl().setUrls(RecordSessionUrl.TYPE_EXCLUDE_FROM_WEBSOCKET, nonEmptyIgnoreList);
      ignoredChannelList = nonEmptyIgnoreList;
    } catch (SQLException e) {
      logger.error(e.getMessage(), e);
     
      ignoredChannelList.clear();
      preparedIgnoredChannels.clear();
     
      throw new WebSocketException("Ignore list could not be applied! Consequently no channel is ignored.");
    } finally {
      // apply to existing channels
      applyChannelIgnoreList();
    }
  }

  public List<String> getChannelIgnoreList() {
    return ignoredChannelList;
  }
 
  private void applyChannelIgnoreList() {
    synchronized (wsProxies) {
      for (Entry<Integer, WebSocketProxy> entry : wsProxies.entrySet()) {
        WebSocketProxy wsProxy = entry.getValue();
       
        if (isChannelIgnored(wsProxy.getDTO())) {
          wsProxy.setForwardOnly(true);
        } else {
          wsProxy.setForwardOnly(false);
        }
      }
    }
  }

  /**
   * If given channel is blacklisted, then nothing should be stored. Moreover
   * it should not appear in user interface, but messages should be forwarded.
   *
   * @param wsProxy
   * @return
   */
  public boolean isChannelIgnored(WebSocketChannelDTO channel) {
    boolean doNotStore = false;
   
    if (config.isForwardAll()) {
      // all channels are blacklisted
      doNotStore = true;
    } else if (!preparedIgnoredChannels.isEmpty()) {
      for (Pattern p : preparedIgnoredChannels) {
        Matcher m = p.matcher(channel.getFullUri());
        if (m.matches()) {
          doNotStore = true;
          break;
        }
      }
    }
   
    return doNotStore;
  }

  @Override
  public void sessionChanged(final Session session) {
    TableWebSocket table = createTableWebSocket();
    if (View.isInitialised()) {
      getWebSocketPanel().setTable(table);
    }
    storage.setTable(table);
   
    try {
      WebSocketProxy.setChannelIdGenerator(table.getMaxChannelId());
    } catch (SQLException e) {
      logger.error("Unable to retrieve current channelId value!", e);
    }
   
    if (fuzzHandler != null) {
      fuzzHandler.resume();
    }

    List<String> ignoredList = new ArrayList<>();
    try {
      List<RecordSessionUrl> recordSessionUrls = Model.getSingleton()
          .getDb().getTableSessionUrl()
          .getUrlsForType(RecordSessionUrl.TYPE_EXCLUDE_FROM_WEBSOCKET);
   
      for (RecordSessionUrl record  : recordSessionUrls) {
        ignoredList.add(record.getUrl());
      }
    } catch (SQLException e) {
      logger.error(e.getMessage(), e);
    } finally {
      try {
        setChannelIgnoreList(ignoredList);
      } catch (WebSocketException e) {
        logger.warn(e.getMessage(), e);
      }
    }
  }

  @Override
  public void sessionAboutToChange(Session session) {
    if (View.isInitialised()) {
      // Prevent the table from being used
      getWebSocketPanel().setTable(null);
      storage.setTable(null);
    }
   
    // close existing connections
    synchronized (wsProxies) {
      for (WebSocketProxy wsProxy : wsProxies.values()) {
        wsProxy.shutdown();
      }
      wsProxies.clear();
    }
   
    if (wsFilterListener != null) {
      wsFilterListener.reset();
    }
   
    if (fuzzHandler != null) {
      fuzzHandler.pause();
    }
  }

  @Override
  public void sessionScopeChanged(Session session) {
    // do nothing
  }

  @Override
  public void sessionModeChanged(Mode mode) {
    this.mode = mode;
  }

  /**
   * Returns false when either in {@link Mode#safe} or in {@link Mode#protect}
   * and the message's channel is not in scope. Call it if you want to do
   * "unsafe" actions like changing payloads, catch breakpoints, send custom
   * messages, etc.
   *
   * @param message
   * @return True if operation on message is not potentially dangerous.
   */
  public boolean isSafe(WebSocketMessageDTO message) {
    if (mode.equals(Mode.safe)) {
      return false;
    } else if (mode.equals(Mode.protect)) {
      return message.isInScope();
    } else {
      return true;
    }
  }
 
  /*
   * ************************************************************************
   * GUI specific code follows here now. It is accessed only by methods hook()
   * and sessionChangedEventHandler() (latter calls only getWebSocketPanel()).
   * All of this UI-related code is private and should not be accessible from
   * outside.
   */


  /**
   * Displayed in the bottom area beside the History, Spider, etc. tabs.
   */
  private WebSocketPanel panel;

  /**
   * Will be added to the hook view.
   */
  private OptionsWebSocketPanel optionsPanel;

  /**
   * Allows to set custom breakpoints, e.g.: for specific opcodes only.
   */
  private WebSocketBreakpointsUiManagerInterface brkManager;
 
  /**
   * WebSockets can be excluded from the current session via this GUI panel.
   */
  private SessionExcludeFromWebSocket sessionExcludePanel;
 
  /**
   * Send custom WebSocket messages.
   */
  private ManualWebSocketSendEditorDialog sendDialog;

  private WebSocketPanel getWebSocketPanel() {
    if (panel == null) {
      panel = new WebSocketPanel(storage.getTable(), getBrkManager());
    }
    return panel;
  }

  private WebSocketBreakpointsUiManagerInterface getBrkManager() {
    if (brkManager == null) {
      ExtensionBreak extBreak = (ExtensionBreak) Control.getSingleton().getExtensionLoader().getExtension(ExtensionBreak.NAME);
      if (extBreak != null) {
        brkManager = new WebSocketBreakpointsUiManagerInterface(extBreak);
      }
    }
    return brkManager;
  }
 
  private AbstractParamPanel getOptionsPanel() {
    if (optionsPanel == null) {
      optionsPanel = new OptionsWebSocketPanel(config);
    }
    return optionsPanel;
  }


  private void initializeWebSocketsForWorkPanel() {
    // Add "HttpPanel" components and views.
    HttpPanelManager manager = HttpPanelManager.getInstance();
   
    // component factory for outgoing and incoming messages with Text view
    HttpPanelComponentFactory componentFactory = new WebSocketComponentFactory();
    manager.addRequestComponentFactory(componentFactory);
    manager.addResponseComponentFactory(componentFactory);

    // use same factory for request & response,
    // as Hex payloads are accessed the same way
    HttpPanelViewFactory viewFactory = new WebSocketHexViewFactory();
    manager.addRequestViewFactory(WebSocketComponent.NAME, viewFactory);
    manager.addResponseViewFactory(WebSocketComponent.NAME, viewFactory);
   
    // add the default Hex view for binary-opcode messages
    HttpPanelDefaultViewSelectorFactory viewSelectorFactory = new HexDefaultViewSelectorFactory();
    manager.addRequestDefaultViewSelectorFactory(WebSocketComponent.NAME, viewSelectorFactory);
    manager.addResponseDefaultViewSelectorFactory(WebSocketComponent.NAME, viewSelectorFactory);

    // replace the normal Text views with the ones that use syntax highlighting
    viewFactory = new SyntaxHighlightTextViewFactory();
    manager.addRequestViewFactory(WebSocketComponent.NAME, viewFactory);
    manager.addResponseViewFactory(WebSocketComponent.NAME, viewFactory);

    // support large payloads on incoming and outgoing messages
    viewFactory = new WebSocketLargePayloadViewFactory();
    manager.addRequestViewFactory(WebSocketComponent.NAME, viewFactory);
    manager.addResponseViewFactory(WebSocketComponent.NAME, viewFactory);
   
    viewSelectorFactory = new WebSocketLargePayloadDefaultViewSelectorFactory();
    manager.addRequestDefaultViewSelectorFactory(WebSocketComponent.NAME, viewSelectorFactory);
    manager.addResponseDefaultViewSelectorFactory(WebSocketComponent.NAME, viewSelectorFactory);
  }
 
  private void clearupWebSocketsForWorkPanel() {
    HttpPanelManager manager = HttpPanelManager.getInstance();
   
    // component factory for outgoing and incoming messages with Text view
    manager.removeRequestComponentFactory(WebSocketComponentFactory.NAME);
    manager.removeRequestComponents(WebSocketComponent.NAME);
    manager.removeResponseComponentFactory(WebSocketComponentFactory.NAME);
    manager.removeResponseComponents(WebSocketComponent.NAME);

    // use same factory for request & response,
    // as Hex payloads are accessed the same way
    manager.removeRequestViewFactory(WebSocketComponent.NAME, WebSocketHexViewFactory.NAME);
    manager.removeResponseViewFactory(WebSocketComponent.NAME, WebSocketHexViewFactory.NAME);
   
    // remove the default Hex view for binary-opcode messages
    manager.removeRequestDefaultViewSelectorFactory(WebSocketComponent.NAME, HexDefaultViewSelectorFactory.NAME);
    manager.removeResponseDefaultViewSelectorFactory(WebSocketComponent.NAME, HexDefaultViewSelectorFactory.NAME);

    // replace the normal Text views with the ones that use syntax highlighting
    manager.removeRequestViewFactory(WebSocketComponent.NAME, SyntaxHighlightTextViewFactory.NAME);
    manager.removeResponseViewFactory(WebSocketComponent.NAME, SyntaxHighlightTextViewFactory.NAME);

    // support large payloads on incoming and outgoing messages
    manager.removeRequestViewFactory(WebSocketComponent.NAME, WebSocketLargePayloadViewFactory.NAME);
    manager.removeResponseViewFactory(WebSocketComponent.NAME, WebSocketLargePayloadViewFactory.NAME);
   
    manager.removeRequestDefaultViewSelectorFactory(WebSocketComponent.NAME, WebSocketLargePayloadDefaultViewSelectorFactory.NAME);
    manager.removeResponseDefaultViewSelectorFactory(WebSocketComponent.NAME, WebSocketLargePayloadDefaultViewSelectorFactory.NAME);
  }

  /**
   * The component returned by this factory contain the normal text view
   * (without syntax highlighting).
   */
    private static final class WebSocketComponentFactory implements HttpPanelComponentFactory {
       
        public static final String NAME = "WebSocketComponentFactory";

        @Override
        public String getName() {
            return NAME;
        }
       
        @Override
        public HttpPanelComponentInterface getNewComponent() {
            return new WebSocketComponent();
        }

        @Override
        public String getComponentName() {
            return WebSocketComponent.NAME;
        }
    }
 
  private static final class WebSocketHexViewFactory implements HttpPanelViewFactory {
       
        public static final String NAME = "WebSocketHexViewFactory";

        @Override
        public String getName() {
            return NAME;
        }
       
        @Override
        public HttpPanelView getNewView() {
      return new HttpPanelHexView(new ByteWebSocketPanelViewModel(), false);
        }

        @Override
        public Object getOptions() {
            return null;
        }
    }
 
  private static final class HexDefaultViewSelector implements HttpPanelDefaultViewSelector {

        public static final String NAME = "HexDefaultViewSelector";
       
        @Override
        public String getName() {
            return NAME;
        }
       
        @Override
        public boolean matchToDefaultView(Message aMessage) {
          // use hex view only when previously selected
//            if (aMessage instanceof WebSocketMessageDTO) {
//                WebSocketMessageDTO msg = (WebSocketMessageDTO)aMessage;
//               
//                return (msg.opcode == WebSocketMessage.OPCODE_BINARY);
//            }
            return false;
        }

        @Override
        public String getViewName() {
            return HttpPanelHexView.NAME;
        }
       
        @Override
        public int getOrder() {
            return 20;
        }
    }

    private static final class HexDefaultViewSelectorFactory implements HttpPanelDefaultViewSelectorFactory {
       
        public static final String NAME = "HexDefaultViewSelectorFactory";
       
        private static HttpPanelDefaultViewSelector defaultViewSelector = null;
       
        private HttpPanelDefaultViewSelector getDefaultViewSelector() {
            if (defaultViewSelector == null) {
                createViewSelector();
            }
            return defaultViewSelector;
        }
       
        private synchronized void createViewSelector() {
            if (defaultViewSelector == null) {
                defaultViewSelector = new HexDefaultViewSelector();
            }
        }
       
        @Override
        public String getName() {
            return NAME;
        }
       
        @Override
        public HttpPanelDefaultViewSelector getNewDefaultViewSelector() {
            return getDefaultViewSelector();
        }
       
        @Override
        public Object getOptions() {
            return null;
        }
    }
 
    private static final class SyntaxHighlightTextViewFactory implements HttpPanelViewFactory {
       
        public static final String NAME = "SyntaxHighlightTextViewFactory";

        @Override
        public String getName() {
            return NAME;
        }
       
        @Override
        public HttpPanelView getNewView() {
            return new WebSocketSyntaxHighlightTextView(new StringWebSocketPanelViewModel());
        }
       
        @Override
        public Object getOptions() {
            return null;
        }
    }
 
  private static final class WebSocketLargePayloadViewFactory implements HttpPanelViewFactory {
   
    public static final String NAME = "WebSocketLargePayloadViewFactory";

    @Override
    public String getName() {
      return NAME;
    }

    @Override
    public HttpPanelView getNewView() {
      return new WebSocketLargePayloadView(new WebSocketLargetPayloadViewModel());
    }

    @Override
    public Object getOptions() {
      return null;
    }
  }
 
  private static final class WebSocketLargePayloadDefaultViewSelectorFactory implements HttpPanelDefaultViewSelectorFactory {
   
    public static final String NAME = "WebSocketLargePayloadDefaultViewSelectorFactory";
    private static HttpPanelDefaultViewSelector defaultViewSelector = null;
   
    private HttpPanelDefaultViewSelector getDefaultViewSelector() {
      if (defaultViewSelector == null) {
        createViewSelector();
      }
      return defaultViewSelector;
    }
   
    private synchronized void createViewSelector() {
      if (defaultViewSelector == null) {
        defaultViewSelector = new WebSocketLargePayloadDefaultViewSelector();
      }
    }
   
    @Override
    public String getName() {
      return NAME;
    }
   
    @Override
    public HttpPanelDefaultViewSelector getNewDefaultViewSelector() {
      return getDefaultViewSelector();
    }
   
    @Override
    public Object getOptions() {
      return null;
    }
  }

  private static final class WebSocketLargePayloadDefaultViewSelector implements HttpPanelDefaultViewSelector {

    public static final String NAME = "WebSocketLargePayloadDefaultViewSelector";
   
    @Override
    public String getName() {
      return NAME;
    }
   
    @Override
    public boolean matchToDefaultView(Message aMessage) {
        return WebSocketLargePayloadUtil.isLargePayload(aMessage);
    }

    @Override
    public String getViewName() {
      return WebSocketLargePayloadView.NAME;
    }
       
        @Override
        public int getOrder() {
          // has to come before HexDefaultViewSelector
            return 15;
        }
  }

  /**
   * This method initializes the dialog for crafting custom messages.
   *
   * @param sender
   *
   * @return
   */
  private ManualWebSocketSendEditorDialog createManualSendDialog(WebSocketPanelSender sender) {
    ManualWebSocketSendEditorDialog sendDialog = new ManualWebSocketSendEditorDialog(getWebSocketPanel().getChannelsModel(), sender, true, "websocket.manual_send");
    sendDialog.setTitle(Constant.messages.getString("websocket.manual_send.menu"));
    return sendDialog;
  }
 
  /**
   * This method initializes the re-send WebSocket message dialog.
   *
   * @param sender
   *
   * @return
   */   
  private ManualWebSocketSendEditorDialog createReSendDialog(WebSocketPanelSender sender) {
    ManualWebSocketSendEditorDialog  resendDialog = new ManualWebSocketSendEditorDialog(getWebSocketPanel().getChannelsModel(), sender, true, "websocket.manual_resend");
    resendDialog.setTitle(Constant.messages.getString("websocket.manual_send.popup"));
    return resendDialog;
  }

  @Override
  public void nodeSelected(SiteNode node) {
    // do nothing
  }

  @Override
  public void onReturnNodeRendererComponent(SiteMapTreeCellRenderer component,
      boolean leaf, SiteNode node) {
    if (leaf) {
      HistoryReference href = component.getHistoryReferenceFromNode(node);
      boolean isWebSocketNode = href != null && href.isWebSocketUpgrade();
      if (isWebSocketNode) {
        boolean isConnected = isConnected(component.getHistoryReferenceFromNode(node));
        boolean isIncluded = node.isIncludedInScope() && !node.isExcludedFromScope();
       
        setWebSocketIcon(isConnected, isIncluded, component);
      }
    }
  }

  private void setWebSocketIcon(boolean isConnected, boolean isIncluded, SiteMapTreeCellRenderer component) {
    if (isConnected) {
      if (isIncluded) {
        component.setIcon(WebSocketPanel.connectTargetIcon);
      } else {
        component.setIcon(WebSocketPanel.connectIcon);
      }
    } else {
      if (isIncluded) {
        component.setIcon(WebSocketPanel.disconnectTargetIcon);
      } else {
        component.setIcon(WebSocketPanel.disconnectIcon);
      }
    }
  }
}
TOP

Related Classes of org.zaproxy.zap.extension.websocket.ExtensionWebSocket

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.