All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.grinder.TCPProxy Maven / Gradle / Ivy

There is a newer version: 4.0.0
Show newest version
// Copyright (C) 2000 Phil Dawes
// Copyright (C) 2000 - 2012 Philip Aston
// Copyright (C) 2001 Paddy Spencer
// Copyright (C) 2003, 2004, 2005 Bertrand Ave
// Copyright (C) 2007 Venelin Mitov
// All rights reserved.
//
// This file is part of The Grinder software distribution. Refer to
// the file LICENSE which is part of The Grinder distribution for
// licensing details. The Grinder distribution is available on the
// Internet at http://grinder.sourceforge.net/
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package net.grinder;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import net.grinder.plugin.http.tcpproxyfilter.ConnectionCache;
import net.grinder.plugin.http.tcpproxyfilter.ConnectionHandlerFactoryImplementation;
import net.grinder.plugin.http.tcpproxyfilter.HTTPRecordingImplementation;
import net.grinder.plugin.http.tcpproxyfilter.HTTPRequestFilter;
import net.grinder.plugin.http.tcpproxyfilter.HTTPResponseFilter;
import net.grinder.plugin.http.tcpproxyfilter.ParametersFromProperties;
import net.grinder.plugin.http.tcpproxyfilter.ProcessHTTPRecordingWithXSLT;
import net.grinder.plugin.http.tcpproxyfilter.ProcessHTTPRecordingWithXSLT.BuiltInStyleSheet;
import net.grinder.plugin.http.tcpproxyfilter.ProcessHTTPRecordingWithXSLT.StyleSheetFile;
import net.grinder.plugin.http.tcpproxyfilter.RegularExpressionsImplementation;
import net.grinder.tools.tcpproxy.CommentSourceImplementation;
import net.grinder.tools.tcpproxy.CompositeFilter;
import net.grinder.tools.tcpproxy.ConnectionDetails;
import net.grinder.tools.tcpproxy.EchoFilter;
import net.grinder.tools.tcpproxy.EndPoint;
import net.grinder.tools.tcpproxy.HTTPProxyTCPProxyEngine;
import net.grinder.tools.tcpproxy.NullFilter;
import net.grinder.tools.tcpproxy.PortForwarderTCPProxyEngine;
import net.grinder.tools.tcpproxy.TCPProxyConsole;
import net.grinder.tools.tcpproxy.TCPProxyEngine;
import net.grinder.tools.tcpproxy.TCPProxyFilter;
import net.grinder.tools.tcpproxy.TCPProxySSLSocketFactory;
import net.grinder.tools.tcpproxy.TCPProxySSLSocketFactoryImplementation;
import net.grinder.tools.tcpproxy.UpdatableCommentSource;
import net.grinder.util.AbstractMainClass;
import net.grinder.util.AttributeStringParserImplementation;
import net.grinder.util.SimpleStringEscaper;
import net.grinder.util.http.URIParserImplementation;

import org.picocontainer.DefaultPicoContainer;
import org.picocontainer.PicoContainer;
import org.picocontainer.behaviors.Caching;
import org.picocontainer.monitors.ConsoleComponentMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * This is the entry point of The TCPProxy process.
 *
 * @author Phil Dawes
 * @author Philip Aston
 * @author Bertrand Ave
 * @author Venelin Mitov
 */
public final class TCPProxy extends AbstractMainClass {

  private static final String USAGE =
    "  java " + TCPProxy.class.getName() + " " +
    "\n\n" +
    "Commonly used options:" +
    "\n  [-http [jython|clojure|]]" +
    "\n                               See below." +
    "\n  [-console]                   Display the console." +
    "\n  [-requestfilter ]    Add a request filter." +
    "\n  [-responsefilter ]   Add a response filter." +
    "\n  [-localhost ]  Default is localhost." +
    "\n  [-localport ]          Default is 8001." +
    "\n  [-keystore ]           Key store details for" +
    "\n  [-keystorepassword ]   SSL certificates." +
    "\n  [-keystoretype ]       Default is JSSE dependent." +
    "\n\n" +
    "Other options:" +
    "\n  [-properties ]         Properties to pass to the filters." +
    "\n  [-remotehost ]    Default is localhost." +
    "\n  [-remoteport ]         Default is 7001." +
    "\n  [-timeout ]         Proxy engine timeout." +
    "\n  [-httpproxy  ]   Route via HTTP/HTTPS proxy." +
    "\n  [-httpsproxy  ]  Override -httpproxy settings for" +
    "\n                               HTTPS." +
    "\n  [-ssl]                       Use SSL when port forwarding." +
    "\n  [-colour]                    Be pretty on ANSI terminals." +
    "\n  [-component ]         Register a component class with" +
    "\n                               the filter PicoContainer." +
    "\n  [-debug]                     Make PicoContainer chatty." +
    "\n\n" +
    " is the name of a class that implements " +
    TCPProxyFilter.class.getName() + " or one of NONE, ECHO. The default " +
    "is ECHO. Multiple filters can be specified for each stream." +
    "\n\n" +
    "By default, the TCPProxy listens as an HTTP/HTTPS Proxy on " +
    "." +
    "\n\n" +
    "If either -remotehost or -remoteport is specified, the TCPProxy " +
    "acts a simple port forwarder between  and " +
    ". Specify -ssl for SSL support." +
    "\n\n" +
    "-http sets up request and response filters to produce a test script " +
    "suitable for use with the HTTP plugin. The keywords 'jython', or " +
    "'clojure' can be used to set the script language; or the filename of " +
    "of an alternative XSLT style sheet can be provided. The default is" +
    "'jython' which creates a Jython script." +
    "\n\n" +
    "-timeout is how long the TCPProxy will wait for a request " +
    "before timing out and freeing the local port. The TCPProxy will " +
    "not time out if there are active connections." +
    "\n\n" +
    "-console displays a simple control window that allows the TCPProxy " +
    "to be shutdown cleanly. This is needed because some shells, e.g. " +
    "Cygwin bash, do not allow Java processes to be interrupted cleanly, " +
    "so filters cannot rely on standard shutdown hooks. " +
    "\n\n" +
    "-httpproxy and -httpsproxy allow output to be directed through " +
    "another HTTP/HTTPS proxy; this may help you reach the Internet. " +
    "These options are not supported in port forwarding mode." +
    "\n\n" +
    "Typical usage: " +
    "\n  java " + TCPProxy.class + " -http -console > grinder.py" +
    "\n\n";

  /**
   * Entry point.
   *
   * @param args Command line arguments.
   */
  public static void main(String[] args) {
    final Logger logger = LoggerFactory.getLogger("tcpproxy");

    try {
      final TCPProxy tcpProxy = new TCPProxy(args, logger);
      tcpProxy.run();
    }
    catch (LoggedInitialisationException e) {
      System.exit(1);
    }
    catch (Throwable e) {
      logger.error("Could not initialise", e);
      System.exit(2);
    }

    System.exit(0);
  }

  private final DefaultPicoContainer m_filterContainer =
    new DefaultPicoContainer(new Caching());

  private final TCPProxyEngine m_proxyEngine;

  /**
   * Package scope for unit tests.
   */
  TCPProxy(String[] args, Logger logger) throws Exception {
    super(logger, USAGE);

    final PrintWriter output = new PrintWriter(System.out);
    m_filterContainer.addComponent(output);

    m_filterContainer.addComponent(logger);

    final UpdatableCommentSource commentSource =
      new CommentSourceImplementation();
    m_filterContainer.addComponent(commentSource);

    // Default values.
    int localPort = 8001;
    String remoteHost = "localhost";
    String localHost = "localhost";
    int remotePort = 7001;
    boolean useSSLPortForwarding = false;
    File keyStoreFile = null;
    char[] keyStorePassword = null;
    String keyStoreType = null;
    boolean isHTTPProxy = true;
    boolean console = false;
    EndPoint chainedHTTPProxy = null;
    EndPoint chainedHTTPSProxy = null;
    int timeout = 0;
    boolean useColour = false;

    final FilterChain requestFilterChain = new FilterChain("request");
    final FilterChain responseFilterChain = new FilterChain("response");

    try {
      // Parse 1.
      for (int i = 0; i < args.length; i++) {
        if ("-properties".equalsIgnoreCase(args[i])) {
          final Properties properties = new Properties();
          final FileInputStream in = new FileInputStream(new File(args[++i]));
          try {
            properties.load(in);
          }
          finally {
            in.close();
          }
          System.getProperties().putAll(properties);
        }
      }

      // Parse 2.
      for (int i = 0; i < args.length; i++) {
        if ("-requestfilter".equalsIgnoreCase(args[i])) {
          requestFilterChain.add(args[++i]);
        }
        else if ("-responsefilter".equalsIgnoreCase(args[i])) {
          responseFilterChain.add(args[++i]);
        }
        else if ("-component".equalsIgnoreCase(args[i])) {
          final Class componentClass;

          try {
            componentClass = Class.forName(args[++i]);
          }
          catch (ClassNotFoundException e) {
            throw barfError("Class '" + args[i] + "' not found.");
          }
          m_filterContainer.addComponent(componentClass);
        }
        else if ("-http".equalsIgnoreCase(args[i])) {
          requestFilterChain.add(HTTPRequestFilter.class);
          responseFilterChain.add(HTTPResponseFilter.class);
          m_filterContainer.addComponent(
            AttributeStringParserImplementation.class);
          m_filterContainer.addComponent(ConnectionCache.class);
          m_filterContainer.addComponent(
            ConnectionHandlerFactoryImplementation.class);
          m_filterContainer.addComponent(ParametersFromProperties.class);
          m_filterContainer.addComponent(HTTPRecordingImplementation.class);
          m_filterContainer.addComponent(ProcessHTTPRecordingWithXSLT.class);
          m_filterContainer.addComponent(
            RegularExpressionsImplementation.class);
          m_filterContainer.addComponent(URIParserImplementation.class);
          m_filterContainer.addComponent(SimpleStringEscaper.class);

          if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
            final String s = args[++i];

            if ("jython".equals(s)) {
              // Default.
            }
            else if ("oldjython".equals(s)) { // Undocumented option.
              m_filterContainer.addComponent(
                BuiltInStyleSheet.TraditionalJython);
            }
            else if ("clojure".equals(s)) {
              m_filterContainer.addComponent(BuiltInStyleSheet.Clojure);
            }
            else {
              m_filterContainer.addComponent(new StyleSheetFile(new File(s)));
            }
          }
        }
        else if ("-localhost".equalsIgnoreCase(args[i])) {
          localHost = args[++i];
        }
        else if ("-localport".equalsIgnoreCase(args[i])) {
          localPort = Integer.parseInt(args[++i]);
        }
        else if ("-remotehost".equalsIgnoreCase(args[i])) {
          remoteHost = args[++i];
          isHTTPProxy = false;
        }
        else if ("-remoteport".equalsIgnoreCase(args[i])) {
          remotePort = Integer.parseInt(args[++i]);
          isHTTPProxy = false;
        }
        else if ("-ssl".equalsIgnoreCase(args[i])) {
          useSSLPortForwarding = true;
        }
        else if ("-keystore".equalsIgnoreCase(args[i])) {
          keyStoreFile = new File(args[++i]);
        }
        else if ("-keystorepassword".equalsIgnoreCase(args[i]) ||
                 "-storepass".equalsIgnoreCase(args[i])) {
          keyStorePassword = args[++i].toCharArray();
        }
        else if ("-keystoretype".equalsIgnoreCase(args[i]) ||
                 "-storetype".equalsIgnoreCase(args[i])) {
          keyStoreType = args[++i];
        }
        else if ("-timeout".equalsIgnoreCase(args[i])) {
          timeout = Integer.parseInt(args[++i]) * 1000;
        }
        else if ("-console".equalsIgnoreCase(args[i])) {
          console = true;
        }
        else if ("-colour".equalsIgnoreCase(args[i]) ||
                 "-color".equalsIgnoreCase(args[i])) {
          useColour = true;
        }
        else if ("-properties".equalsIgnoreCase(args[i])) {
          /* Already handled */
          ++i;
        }
        else if ("-httpproxy".equalsIgnoreCase(args[i])) {
          chainedHTTPProxy =
            new EndPoint(args[++i], Integer.parseInt(args[++i]));
        }
        else if ("-httpsproxy".equalsIgnoreCase(args[i])) {
          chainedHTTPSProxy =
            new EndPoint(args[++i], Integer.parseInt(args[++i]));
        }
        else if ("-debug".equalsIgnoreCase(args[i])) {
          m_filterContainer.changeMonitor(
            new ConsoleComponentMonitor(System.err));
        }
        else {
          throw barfUsage();
        }
      }
    }
    catch (FileNotFoundException fnfe) {
      throw barfError(fnfe.getMessage());
    }
    catch (IndexOutOfBoundsException e) {
      throw barfUsage();
    }
    catch (NumberFormatException e) {
      throw barfUsage();
    }

    if (timeout < 0) {
      throw barfError("Timeout must be non-negative.");
    }

    final EndPoint localEndPoint = new EndPoint(localHost, localPort);
    final EndPoint remoteEndPoint = new EndPoint(remoteHost, remotePort);

    if (chainedHTTPSProxy == null && chainedHTTPProxy != null) {
      chainedHTTPSProxy = chainedHTTPProxy;
    }

    if (chainedHTTPSProxy != null && !isHTTPProxy) {
      throw barfError("Routing through a HTTP/HTTPS proxy is not supported " +
                      "in port forwarding mode.");
    }

    final TCPProxyFilter requestFilter = requestFilterChain.resolveFilter();
    final TCPProxyFilter responseFilter = responseFilterChain.resolveFilter();

    final StringBuilder startMessage = new StringBuilder();

    startMessage.append("Initialising as ");

    if (isHTTPProxy) {
      startMessage.append("an HTTP/HTTPS proxy");
    }
    else {
      if (useSSLPortForwarding) {
        startMessage.append("an SSL port forwarder");
      }
      else {
        startMessage.append("a TCP port forwarder");
      }
    }

    startMessage.append(" with the parameters:");
    startMessage.append("\n   Request filters:    ");
    startMessage.append(requestFilter);
    startMessage.append("\n   Response filters:   ");
    startMessage.append(responseFilter);
    startMessage.append("\n   Local address:      " + localEndPoint);

    if (!isHTTPProxy) {
      startMessage.append("\n   Remote address:     " + remoteEndPoint);
    }

    if (chainedHTTPProxy != null) {
      startMessage.append("\n   HTTP proxy:         " + chainedHTTPProxy);
    }

    if (chainedHTTPSProxy != null) {
      startMessage.append("\n   HTTPS proxy:        " + chainedHTTPSProxy);
    }

    if (keyStoreFile != null) {
      startMessage.append("\n   Key store:          ");
      startMessage.append(keyStoreFile.toString());

      // Key store password is optional.
      if (keyStorePassword != null) {
        startMessage.append("\n   Key store password: ");
        for (int i = 0; i < keyStorePassword.length; ++i) {
          startMessage.append('*');
        }
      }

      // Key store type can be null => use whatever
      // KeyStore.getDefaultType() says (we can't print the default
      // here without loading the JSSE).
      if (keyStoreType != null) {
        startMessage.append("\n   Key store type:     " + keyStoreType);
      }
    }

    logger.info(startMessage.toString());

    final TCPProxySSLSocketFactory sslSocketFactory =
      keyStoreFile != null ?
      new TCPProxySSLSocketFactoryImplementation(keyStoreFile,
                                                 keyStorePassword,
                                                 keyStoreType) :
      new TCPProxySSLSocketFactoryImplementation();

    m_filterContainer.start();

    if (isHTTPProxy) {
      m_proxyEngine =
        new HTTPProxyTCPProxyEngine(
          sslSocketFactory,
          requestFilter, responseFilter,
          output,
          logger,
          localEndPoint,
          useColour,
          timeout,
          chainedHTTPProxy, chainedHTTPSProxy);
    }
    else {
      if (useSSLPortForwarding) {
        m_proxyEngine =
          new PortForwarderTCPProxyEngine(
            sslSocketFactory,
            requestFilter, responseFilter,
            output,
            logger,
            new ConnectionDetails(localEndPoint, remoteEndPoint, true),
            useColour,
            timeout);
      }
      else {
        m_proxyEngine =
          new PortForwarderTCPProxyEngine(
            requestFilter, responseFilter,
            output,
            logger,
            new ConnectionDetails(localEndPoint, remoteEndPoint, false),
            useColour,
            timeout);
      }
    }

    if (console) {
      new TCPProxyConsole(m_proxyEngine, commentSource);
    }

    logger.info("Engine initialised, listening on port " + localPort);
  }

  private void run() {
    final Runnable shutdown = new Runnable() {
      private boolean m_stopped = false;

      public synchronized void run() {
        if (!m_stopped) {
          m_stopped = true;
          m_proxyEngine.stop();
          m_filterContainer.stop();
          m_filterContainer.dispose();
        }
      }
    };

    Runtime.getRuntime().addShutdownHook(new Thread(shutdown));

    m_proxyEngine.run();
    shutdown.run();

    getLogger().info("Engine exited");
  }

  private final class FilterChain {
    private final String m_type;
    private final List m_filterKeys = new ArrayList();
    private int m_value;

    public FilterChain(String type) {
      m_type = type;
    }

    public void add(Class theClass) {
      final String key = m_type + ++m_value;
      m_filterContainer.addComponent(key, theClass);
      m_filterKeys.add(key);
    }

    @SuppressWarnings("unchecked")
    public void add(String filterClassName)
      throws LoggedInitialisationException {

      if ("NONE".equals(filterClassName)) {
        add(NullFilter.class);
      }
      else if ("ECHO".equals(filterClassName)) {
        add(EchoFilter.class);
      }
      else {
        final Class filterClass;

        try {
          filterClass = Class.forName(filterClassName);
        }
        catch (ClassNotFoundException e) {
          throw barfError("Class '" + filterClassName + "' not found.");
        }

        if (!TCPProxyFilter.class.isAssignableFrom(filterClass)) {
          throw barfError("The class '" + filterClass.getName() +
                          "' does not implement the interface: '" +
                          TCPProxyFilter.class.getName() + "'.");
        }

        add((Class) filterClass);
      }
    }

    public TCPProxyFilter resolveFilter() {
      if (m_filterKeys.size() == 0) {
        add(EchoFilter.class);
      }

      final CompositeFilter result = new CompositeFilter();

      for (String key : m_filterKeys) {
        result.add((TCPProxyFilter) m_filterContainer.getComponent(key));
      }

      return result;
    }
  }

  /**
   *  Accessor for unit tests.
   */
  PicoContainer getFilterContainer() {
    return m_filterContainer;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy