Skip to content

Commit

Permalink
Introduce SSL support
Browse files Browse the repository at this point in the history
PoxyProxy can now listen on SSL ports.
  • Loading branch information
ethomson committed Oct 21, 2018
1 parent 763a649 commit 0162afb
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 44 deletions.
81 changes: 63 additions & 18 deletions src/main/java/com/microsoft/tfs/tools/poxy/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ public class Options
*/
private volatile int localPort = 8000;

/**
* Local TCP port to bind to (SSL/TLS).
*/
private volatile int localSSLPort = 0;

/**
* SSL Keystore file path.
*/
private volatile String sslKeystoreFile = null;

/**
* Password for SSL Keystore file.
*/
private volatile String sslKeystorePassword = null;

/**
* If a connection to a server or forward proxy takes longer than this many
* seconds, it errors with 504 Gateway Timeout.
Expand Down Expand Up @@ -125,6 +140,36 @@ public void setLocalPort(int localPort)
this.localPort = localPort;
}

public int getLocalSSLPort()
{
return this.localSSLPort;
}

public void setLocalSSLPort(int localSSLPort)
{
this.localSSLPort = localSSLPort;
}

public String getSSLKeystoreFile()
{
return this.sslKeystoreFile;
}

public void setSSLKeystoreFile(String sslKeystoreFile)
{
this.sslKeystoreFile = sslKeystoreFile;
}

public String getSSLKeystorePassword()
{
return this.sslKeystorePassword;
}

public void setSSLKeystorePassword(String sslKeystorePassword)
{
this.sslKeystorePassword = sslKeystorePassword;
}

public int getConnectTimeoutSeconds()
{
return this.connectTimeoutSeconds;
Expand Down Expand Up @@ -298,40 +343,40 @@ public Set<String> getForwardProxyBypassHosts()

public boolean isAuthenticationRequired()
{
return authenticationRequired;
return authenticationRequired;
}

public void setAuthenticationRequired(boolean required)
{
this.authenticationRequired = required;
this.authenticationRequired = required;
}

public void setProxyCredentials(List<String> credentials)
{
synchronized (proxyCredentials)
{
for (String credential : credentials)
{
String[] parts = credential.split(":", 2);
proxyCredentials.put(parts[0], parts[1]);
}
}
synchronized (proxyCredentials)
{
for (String credential : credentials)
{
String[] parts = credential.split(":", 2);
proxyCredentials.put(parts[0], parts[1]);
}
}
}

public void addProxyCredential(String username, String password)
{
synchronized (proxyCredentials)
{
proxyCredentials.put(username, password);
}
synchronized (proxyCredentials)
{
proxyCredentials.put(username, password);
}
}

public boolean credentialsMatchProxyCredentials(String username, String password)
{
synchronized (proxyCredentials)
{
return password.equals(proxyCredentials.get(username));
}
synchronized (proxyCredentials)
{
return password.equals(proxyCredentials.get(username));
}
}

public int getMaxHeaderSizeBytes()
Expand Down
138 changes: 112 additions & 26 deletions src/main/java/com/microsoft/tfs/tools/poxy/Poxy.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
/*
* Poxy: a simple HTTP proxy for testing.
*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
*/

package com.microsoft.tfs.tools.poxy;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;

import com.microsoft.tfs.tools.poxy.GetOptions.Option;
import com.microsoft.tfs.tools.poxy.GetOptions.OptionException;
import com.microsoft.tfs.tools.poxy.logger.LogLevel;
Expand All @@ -39,6 +45,7 @@ public Poxy(final String[] args)
private static void usage()
{
System.err.println("Usage: Poxy [-d|--debug] [-a|--address address] [-p|--port port]");
System.err.println(" [-s|--ssl-port port] [--ssl-keystore file] [--ssl-keystore-password pass]");
System.err.println(" [--max-threads num] [--connect-timeout secs]");
System.err.println(" [--socket-read-timeout secs] [--forward-proxy url]");
System.err.println(" [--forward-proxy-bypass host1,...] [--default-domain domain]");
Expand All @@ -47,7 +54,9 @@ private static void usage()

public void run()
{
final ArrayList<Thread> listenerThreads = new ArrayList<Thread>();
final Options options = getOptionsAndConfigureLogging();

if (options == null)
{
System.exit(1);
Expand All @@ -57,26 +66,81 @@ public void run()

try
{
@SuppressWarnings("resource")
final ServerSocket serverSocket = new ServerSocket(options.getLocalPort(), 4096, InetAddress.getByName(options.getLocalAddress()));
final ServerSocket httpSocket = new ServerSocket(options.getLocalPort(), 4096,
InetAddress.getByName(options.getLocalAddress()));
listenerThreads.add(new Thread(new SocketListener(httpSocket, executorService, options)));

while (true)
if (options.getLocalSSLPort() != 0)
{
final Socket connectionSocket = serverSocket.accept();
executorService.submit(new Connection(connectionSocket, options, executorService));
final SSLContext sslContext = configureSSL(options);
final ServerSocket httpsSocket = new ServerSocket(options.getLocalSSLPort(), 4096,
InetAddress.getByName(options.getLocalAddress()));
listenerThreads
.add(new Thread(new SSLSocketListener(httpsSocket, executorService, options, sslContext)));
}
}
catch (GeneralSecurityException e)
{
logger.write(LogLevel.FATAL, "Could not configure SSL", e);
System.exit(1);
}
catch (IOException e)
{
logger.write(LogLevel.FATAL, "Could not start server", e);
System.exit(1);
}

for (Thread t : listenerThreads)
{
t.start();
}

try
{
for (Thread t : listenerThreads)
{
t.join();
}
}
catch (InterruptedException e)
{
logger.write(LogLevel.FATAL, "Failed to listen to server sockets");
System.exit(1);
}
}

private SSLContext configureSSL(Options options) throws GeneralSecurityException
{
final SSLContext sslContext = SSLContext.getInstance("TLSv1.2");

if (options.getSSLKeystoreFile() != null)
{
try
{
final KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

keyStore.load(new FileInputStream(options.getSSLKeystoreFile()),
options.getSSLKeystorePassword().toCharArray());

keyManagerFactory.init(keyStore, options.getSSLKeystorePassword().toCharArray());

sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
}
catch (IOException e)
{
throw new GeneralSecurityException("Could not open keystore file", e);
}
}

return sslContext;
}

/**
* Parses options and configures the logging (with debug enabled if that
* option was set).
*
* Parses options and configures the logging (with debug enabled if that option
* was set).
*
* @return the options or <code>null</code> if an error happened
*/
private Options getOptionsAndConfigureLogging()
Expand All @@ -85,32 +149,30 @@ private Options getOptionsAndConfigureLogging()
Logger.setLevel(LogLevel.INFO);

/* Setup command-line options (with defaults) */
final Option[] availableOptions =
new Option[]
{
final Option[] availableOptions = new Option[] {
/* Bind on port 8000 */
new Option("address", 'a', true, "0.0.0.0"),
new Option("port", 'p', true, "8000"),
new Option("address", 'a', true, "0.0.0.0"), new Option("port", 'p', true, "8000"),

/* SSL configuration */
new Option("ssl-port", 's', true), new Option("ssl-keystore", true),
new Option("ssl-keystore-password", true),

/* Allow debugging */
new Option("debug", 'd'),

/* IO */
new Option("max-threads", true),
new Option("connect-timeout", true),
new Option("max-threads", true), new Option("connect-timeout", true),
new Option("socket-read-timeout", true),

/* Proxy chaining */
new Option("forward-proxy", true),
new Option("forward-proxy-bypass", true, true),
new Option("forward-proxy", true), new Option("forward-proxy-bypass", true, true),
new Option("default-domain", true),

/* Authentication */
new Option("credentials", true, true),

/* Debugging aids */
new Option("add-response-delay", true, "0"),
};
new Option("add-response-delay", true, "0"), };

final GetOptions getOptions = new GetOptions(availableOptions);

Expand Down Expand Up @@ -154,6 +216,21 @@ private Options getOptionsAndConfigureLogging()
proxyOptions.setLocalPort(Integer.parseInt(getOptions.getArgument("port")));
}

if (getOptions.getArgument("ssl-port") != null)
{
proxyOptions.setLocalSSLPort(Integer.parseInt(getOptions.getArgument("ssl-port")));
}

if (getOptions.getArgument("ssl-keystore") != null)
{
proxyOptions.setSSLKeystoreFile(getOptions.getArgument("ssl-keystore"));
}

if (getOptions.getArgument("ssl-keystore-password") != null)
{
proxyOptions.setSSLKeystorePassword(getOptions.getArgument("ssl-keystore-password"));
}

if (getOptions.getArgument("max-threads") != null)
{
proxyOptions.setMaxThreads(Integer.parseInt(getOptions.getArgument("max-threads")));
Expand All @@ -166,12 +243,14 @@ private Options getOptionsAndConfigureLogging()

if (getOptions.getArgument("socket-read-timeout") != null)
{
proxyOptions.setSocketReadTimeoutSeconds(Integer.parseInt(getOptions.getArgument("socket-read-timeout")));
proxyOptions
.setSocketReadTimeoutSeconds(Integer.parseInt(getOptions.getArgument("socket-read-timeout")));
}

if (getOptions.getArgument("add-response-delay") != null)
{
proxyOptions.setResponseDelayMilliseconds(Integer.parseInt(getOptions.getArgument("add-response-delay")));
proxyOptions
.setResponseDelayMilliseconds(Integer.parseInt(getOptions.getArgument("add-response-delay")));
}
}
catch (NumberFormatException e)
Expand All @@ -188,14 +267,21 @@ private Options getOptionsAndConfigureLogging()
proxyOptions.setForwardProxyBypassHosts(getOptions.getArguments("forward-proxy-bypass"));
proxyOptions.setForwardProxyBypassHostDefaultDomain(getOptions.getArgument("default-domain"));
}

if (getOptions.getArgument("credentials") != null)
{
proxyOptions.setAuthenticationRequired(true);
proxyOptions.setProxyCredentials(getOptions.getArguments("credentials"));
}

logger.write(LogLevel.INFO, "Starting server on " + proxyOptions.getLocalAddress() + ":" + Integer.toString(proxyOptions.getLocalPort()));
logger.write(LogLevel.INFO, "Starting server on " + proxyOptions.getLocalAddress() + ":"
+ Integer.toString(proxyOptions.getLocalPort()));

if (proxyOptions.getLocalSSLPort() != 0)
{
logger.write(LogLevel.INFO, "Starting TLS server on " + proxyOptions.getLocalAddress() + ":"
+ Integer.toString(proxyOptions.getLocalSSLPort()));
}

return proxyOptions;
}
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/com/microsoft/tfs/tools/poxy/SSLSocketListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.microsoft.tfs.tools.poxy;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;

public class SSLSocketListener extends SocketListener
{
private final SSLContext sslContext;

public SSLSocketListener(ServerSocket serverSocket, ExecutorService executorService, Options options,
SSLContext sslContext)
{
super(serverSocket, executorService, options);

this.sslContext = sslContext;
}

@Override
protected Socket accept() throws Exception
{
Socket rawSocket = getServerSocket().accept();

final SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(rawSocket, null,
rawSocket.getPort(), false);
sslSocket.setUseClientMode(false);

return sslSocket;
}
}
Loading

0 comments on commit 0162afb

Please sign in to comment.