diff --git a/.travis.yml b/.travis.yml
index fa369a5af..45201c819 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,6 @@ sudo: false
language: java
jdk:
- - oraclejdk7
- oraclejdk8
cache:
diff --git a/README.md b/README.md
index d512855e6..2f39d65e5 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,28 @@
-[](https://travis-ci.org/adamfisk/LittleProxy)
+[](https://travis-ci.com/mrog/LittleProxy)
+[](https://depshield.github.io)
-LittleProxy is a high performance HTTP proxy written in Java atop Trustin Lee's excellent [Netty](http://netty.io) event-based networking library. It's quite stable, performs well, and is easy to integrate into your projects.
+This is an updated fork of adamfisk's LittleProxy. The original project appears
+to have been abondoned. Because it's so incredibly useful, it's being brought
+back to life in this repository.
+
+LittleProxy is a high performance HTTP proxy written in Java atop Trustin Lee's
+excellent [Netty](http://netty.io) event-based networking library. It's quite
+stable, performs well, and is easy to integrate into your projects.
One option is to clone LittleProxy and run it from the command line. This is as simple as:
```
-$ git clone git://github.com/adamfisk/LittleProxy.git
+$ git clone git@github.com:mrog/LittleProxy.git
$ cd LittleProxy
$ ./run.bash
```
You can embed LittleProxy in your own projects through Maven with the following:
-
```
- org.littleshoot
+ xyz.rogfam
littleproxy
- 1.1.2
+ 2.0.0-beta-1
```
@@ -143,11 +149,13 @@ For examples of configuring logging, see [src/test/resources/log4j.xml](src/test
If you have questions, please visit our Google Group here:
-https://groups.google.com/forum/#!forum/littleproxy
+https://groups.google.com/forum/#!forum/littleproxy2
+
+(The original group at https://groups.google.com/forum/#!forum/littleproxy isn't
+accepting posts from new users. But it's still a great resource if you're
+searching for older answers.)
-To subscribe, send an E-Mail to mailto:LittleProxy+subscribe@googlegroups.com.
-Simply answering, don't clicking the button, bypasses Googles registration
-process. You will become a member.
+To subscribe, send an e-mail to [LittleProxy2+subscribe@googlegroups.com](mailto:LittleProxy2+subscribe@googlegroups.com).
Benchmarking instructions and results can be found [here](performance).
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
new file mode 100644
index 000000000..2d6af9af2
--- /dev/null
+++ b/RELEASE_NOTES.md
@@ -0,0 +1,10 @@
+# Release Notes
+
+- 2.0.0-beta-1
+ - New Maven coordinates
+ - Moved from Java 7 to 8
+ - Updated dependency versions
+ - Made client details available to ChainedProxyManager
+ - Refactored MITM manager to accept engine with user-defined parameters
+ - Added ability to load keystore from classpath
+
\ No newline at end of file
diff --git a/littleproxy_cert b/littleproxy_cert
index f609f3ba6..d31898a2d 100644
Binary files a/littleproxy_cert and b/littleproxy_cert differ
diff --git a/pom.xml b/pom.xml
index 351b006d3..70db20a37 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,22 +1,22 @@
4.0.0
- org.littleshoot
+ xyz.rogfam
littleproxy
jar
- 1.1.3-SNAPSHOT
+ 2.0.0-beta-1
LittleProxy
LittleProxy is a high performance HTTP proxy written in Java and using the Netty networking framework.
- http://littleproxy.org
+ https://github.com/mrog/LittleProxy
UTF-8
UTF-8
github
- 4.0.44.Final
- 1.7.24
- 1.7
+ 4.1.33.Final
+ 1.7.25
+ 1.8
@@ -43,13 +43,13 @@
github
- https://github.com/adamfisk/LittleProxy/issues
+ https://github.com/mrog/LittleProxy/issues
- scm:git:https://adamfisk@github.com/adamfisk/LittleProxy.git
- scm:git:git@github.com:adamfisk/LittleProxy
- scm:git:git@github.com:adamfisk/LittleProxy.git
+ scm:git:https://github.com/mrog/LittleProxy.git
+ scm:git:git@github.com:mrog/LittleProxy.git
+ scm:git:https://github.com/mrog/LittleProxy
HEAD
@@ -70,33 +70,20 @@
- doclint-java8-disable
-
- [1.8,)
-
-
- -Xdoclint:none
-
-
-
- doclint-java7-earlier
+ release
- [,1.8)
+
+ performRelease
+ true
+
-
-
-
-
-
-
- release
org.apache.maven.plugins
maven-surefire-plugin
- 2.19.1
+ 2.22.1
true
@@ -104,7 +91,6 @@
org.apache.maven.plugins
maven-source-plugin
- 3.0.1
attach-sources
@@ -118,11 +104,10 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 2.10.4
${java.version}
- ${javadoc.opts}
+ none
@@ -145,13 +130,22 @@
sign
+
+ ${gpg.keyname}
+ ${gpg.keyname}
+ gpg
+
+ --pinentry-mode
+ loopback
+
+
org.sonatype.plugins
nexus-staging-maven-plugin
- 1.6.7
+ 1.6.8
true
ossrh
@@ -162,7 +156,6 @@
org.apache.maven.plugins
maven-release-plugin
- 2.5.3
true
false
@@ -173,26 +166,19 @@
-
-
- netty-4.1
-
- 4.1.8.Final
-
-
com.google.guava
guava
- 23.0
+ 27.0.1-jre
commons-cli
commons-cli
- 1.3.1
+ 1.4
true
@@ -200,7 +186,7 @@
org.apache.commons
commons-lang3
- 3.5
+ 3.8.1
@@ -213,35 +199,35 @@
org.hamcrest
hamcrest-core
- 1.3
+ 2.1
test
org.hamcrest
hamcrest-library
- 1.3
+ 2.1
test
org.eclipse.jetty
jetty-server
- 8.1.17.v20150415
+ 8.1.22.v20160922
test
org.mockito
mockito-core
- 2.7.12
+ 2.24.0
test
org.mock-server
mockserver-netty
- 3.10.4
+ 5.5.1
test
@@ -254,7 +240,7 @@
org.seleniumhq.selenium
selenium-java
- 2.53.1
+ 3.141.59
test
@@ -265,16 +251,16 @@
- log4j
- log4j
- 1.2.17
+ org.apache.logging.log4j
+ log4j-core
+ 2.11.2
true
org.apache.httpcomponents
httpclient
- 4.5.3
+ 4.5.7
test
@@ -415,13 +401,13 @@
org.apache.maven.plugins
maven-enforcer-plugin
- 1.4.1
+ 3.0.0-M2
org.apache.maven.plugins
maven-site-plugin
- 3.6
+ 3.7.1
@@ -433,13 +419,13 @@
org.apache.maven.plugins
maven-dependency-plugin
- 2.10
+ 3.1.1
org.apache.maven.plugins
maven-clean-plugin
- 3.0.0
+ 3.1.0
@@ -457,13 +443,25 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.0.2
+ 3.1.1
org.apache.maven.plugins
maven-resources-plugin
- 3.0.2
+ 3.1.0
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.0.1
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.0.1
@@ -472,7 +470,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.19.1
+ 2.22.1
-Xmx1g -XX:MaxPermSize=256m
@@ -481,7 +479,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.6.0
+ 3.8.0
${java.version}
${java.version}
@@ -492,7 +490,6 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 2.10.4
private
${java.version}
@@ -500,14 +497,14 @@
http://netty.io/4.0/api/
- ${javadoc.opts}
+ none
org.apache.maven.plugins
maven-shade-plugin
- 2.4.3
+ 3.2.1
package
@@ -557,12 +554,10 @@
org.apache.maven.plugins
maven-site-plugin
- 3.4
maven-dependency-plugin
- 2.10
@@ -574,7 +569,7 @@
org.apache.maven.plugins
maven-project-info-reports-plugin
- 2.9
+ 3.0.0
true
true
@@ -583,7 +578,6 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 2.10.3
private
${java.version}
@@ -591,13 +585,13 @@
http://netty.io/4.0/api/
- ${javadoc.opts}
+ none
org.apache.maven.plugins
maven-surefire-report-plugin
- 2.19.1
+ 2.22.1
false
@@ -605,7 +599,7 @@
org.apache.maven.plugins
maven-checkstyle-plugin
- 2.17
+ 3.0.0
org.apache.maven.plugins
@@ -627,7 +621,7 @@
org.codehaus.mojo
findbugs-maven-plugin
- 3.0.4
+ 3.0.5
@@ -635,12 +629,12 @@
org.apache.maven.plugins
maven-jxr-plugin
- 2.5
+ 3.0.0
org.apache.maven.plugins
maven-pmd-plugin
- 3.7
+ 3.11.0
true
utf-8
@@ -656,7 +650,7 @@
org.codehaus.mojo
versions-maven-plugin
- 2.3
+ 2.7
@@ -690,23 +684,13 @@
- afisk
- Adam fisk
- a@littleshoot.org
- LittleShoot
- http://www.littleshoot.org/
- Developer
- -5
-
-
-
- ox.to.a.cart
- Ox Cart
- ox@getlantern.org
- GetLantern
- https://www.getlantern.org/
+ mrogers
+ Mark Rogers
+ mark.rogers@gmail.com
+ Mark Rogers
+ https://github.com/mrog
Developer
- -5
+ -7
diff --git a/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java b/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java
index f73c69cb3..e7123d319 100644
--- a/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java
+++ b/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java
@@ -1,6 +1,7 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpRequest;
+import org.littleshoot.proxy.impl.ClientDetails;
import java.util.Queue;
@@ -32,7 +33,9 @@ public interface ChainedProxyManager {
*
* @param httpRequest
* @param chainedProxies
+ * @param clientDetails
*/
void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies);
+ Queue chainedProxies,
+ ClientDetails clientDetails);
}
\ No newline at end of file
diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
index 367dc8dd1..62b0ee8b7 100644
--- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
+++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
@@ -335,4 +335,18 @@ HttpProxyServerBootstrap withConnectTimeout(
* @return proxy server bootstrap for chaining
*/
HttpProxyServerBootstrap withThreadPoolConfiguration(ThreadPoolConfiguration configuration);
+
+ /**
+ * Specifies if the proxy server should accept a proxy protocol header. Once set it works with request that
+ * include a proxy protocol header. The proxy server reads an incoming proxy protocol header from the
+ * client.
+ * @param allowProxyProtocol when true, the proxy will accept a proxy protocol header
+ */
+ HttpProxyServerBootstrap withAcceptProxyProtocol(boolean allowProxyProtocol);
+
+ /**
+ * Specifies if the proxy server should send a proxy protocol header.
+ * @param sendProxyProtocol when true, the proxy will send a proxy protocol header
+ */
+ HttpProxyServerBootstrap withSendProxyProtocol(boolean sendProxyProtocol);
}
\ No newline at end of file
diff --git a/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java b/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java
new file mode 100644
index 000000000..a58fb928a
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java
@@ -0,0 +1,24 @@
+package org.littleshoot.proxy.extras;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+
+/**
+ * Encodes an HAProxy proxy protocol header
+ *
+ * @see Proxy Protocol Specification
+ */
+public class HAProxyMessageEncoder extends MessageToByteEncoder {
+
+ @Override
+ protected void encode(ChannelHandlerContext ctx, ProxyProtocolMessage msg, ByteBuf out) {
+ out.writeBytes(getHaProxyMessage(msg));
+ }
+
+ private byte [] getHaProxyMessage(ProxyProtocolMessage msg) {
+ return String.format("%s %s %s %s %s %s\r\n", msg.getCommand(), msg.getProxiedProtocol(), msg.getSourceAddress(), msg.getDestinationAddress(), msg.getSourcePort(),
+ msg.getDestinationPort()).getBytes();
+ }
+
+}
diff --git a/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java b/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java
new file mode 100644
index 000000000..cee89ad06
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java
@@ -0,0 +1,66 @@
+package org.littleshoot.proxy.extras;
+
+import io.netty.handler.codec.haproxy.HAProxyCommand;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+import io.netty.handler.codec.haproxy.HAProxyProtocolVersion;
+import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
+
+public class ProxyProtocolMessage {
+
+ private HAProxyProtocolVersion protocolVersion;
+ private HAProxyCommand command;
+ private HAProxyProxiedProtocol proxiedProtocol;
+ private String sourceAddress;
+ private String destinationAddress;
+ private int sourcePort;
+ private int destinationPort;
+
+ public ProxyProtocolMessage(HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol, String sourceAddress, String destinationAddress
+ , int sourcePort, int destinationPort) {
+ this.protocolVersion = protocolVersion;
+ this.command = command;
+ this.proxiedProtocol = proxiedProtocol;
+ this.sourceAddress = sourceAddress;
+ this.destinationAddress = destinationAddress;
+ this.sourcePort = sourcePort;
+ this.destinationPort = destinationPort;
+ }
+
+ public ProxyProtocolMessage(HAProxyMessage haProxyMessage) {
+ this.protocolVersion = haProxyMessage.protocolVersion();
+ this.command = haProxyMessage.command();
+ this.proxiedProtocol = haProxyMessage.proxiedProtocol();
+ this.sourceAddress = haProxyMessage.sourceAddress();
+ this.destinationAddress = haProxyMessage.destinationAddress();
+ this.sourcePort = haProxyMessage.sourcePort();
+ this.destinationPort = haProxyMessage.destinationPort();
+ }
+
+ public HAProxyProtocolVersion getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ public HAProxyCommand getCommand() {
+ return command;
+ }
+
+ public HAProxyProxiedProtocol getProxiedProtocol() {
+ return proxiedProtocol;
+ }
+
+ public String getSourceAddress() {
+ return sourceAddress;
+ }
+
+ public String getDestinationAddress() {
+ return destinationAddress;
+ }
+
+ public int getSourcePort() {
+ return sourcePort;
+ }
+
+ public int getDestinationPort() {
+ return destinationPort;
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java b/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java
index 45c71fe69..190eacaa3 100644
--- a/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java
+++ b/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java
@@ -10,8 +10,15 @@
* {@link MitmManager} that uses self-signed certs for everything.
*/
public class SelfSignedMitmManager implements MitmManager {
- SelfSignedSslEngineSource selfSignedSslEngineSource =
- new SelfSignedSslEngineSource(true);
+ private final SelfSignedSslEngineSource selfSignedSslEngineSource;
+
+ public SelfSignedMitmManager() {
+ this.selfSignedSslEngineSource = new SelfSignedSslEngineSource(true);
+ }
+
+ public SelfSignedMitmManager(SelfSignedSslEngineSource selfSignedSslEngineSource) {
+ this.selfSignedSslEngineSource = selfSignedSslEngineSource;
+ }
@Override
public SSLEngine serverSslEngine(String peerHost, int peerPort) {
diff --git a/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java b/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java
index 31b671cdd..7b9ec0947 100644
--- a/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java
+++ b/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java
@@ -16,6 +16,9 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.CertificateException;
@@ -31,24 +34,30 @@ public class SelfSignedSslEngineSource implements SslEngineSource {
private static final Logger LOG = LoggerFactory
.getLogger(SelfSignedSslEngineSource.class);
- private static final String ALIAS = "littleproxy";
- private static final String PASSWORD = "Be Your Own Lantern";
private static final String PROTOCOL = "TLS";
- private final File keyStoreFile;
+
+ private final String alias;
+ private final String password;
+ private final String keyStoreFile;
private final boolean trustAllServers;
private final boolean sendCerts;
private SSLContext sslContext;
- public SelfSignedSslEngineSource(String keyStorePath,
- boolean trustAllServers, boolean sendCerts) {
+ public SelfSignedSslEngineSource(String keyStorePath, boolean trustAllServers, boolean sendCerts,
+ String alias, String password) {
this.trustAllServers = trustAllServers;
this.sendCerts = sendCerts;
- this.keyStoreFile = new File(keyStorePath);
- initializeKeyStore();
+ this.keyStoreFile = keyStorePath;
+ this.alias = alias;
+ this.password = password;
initializeSSLContext();
}
+ public SelfSignedSslEngineSource(String keyStorePath, boolean trustAllServers, boolean sendCerts) {
+ this(keyStorePath, trustAllServers, sendCerts, "littleproxy", "Be Your Own Lantern");
+ }
+
public SelfSignedSslEngineSource(String keyStorePath) {
this(keyStorePath, false, true);
}
@@ -79,19 +88,14 @@ public SSLContext getSslContext() {
return sslContext;
}
- private void initializeKeyStore() {
- if (keyStoreFile.isFile()) {
- LOG.info("Not deleting keystore");
- return;
- }
-
- nativeCall("keytool", "-genkey", "-alias", ALIAS, "-keysize",
+ private void initializeKeyStore(String filename) {
+ nativeCall("keytool", "-genkey", "-alias", alias, "-keysize",
"4096", "-validity", "36500", "-keyalg", "RSA", "-dname",
- "CN=littleproxy", "-keypass", PASSWORD, "-storepass",
- PASSWORD, "-keystore", keyStoreFile.getName());
+ "CN=littleproxy", "-keypass", password, "-storepass",
+ password, "-keystore", filename);
- nativeCall("keytool", "-exportcert", "-alias", ALIAS, "-keystore",
- keyStoreFile.getName(), "-storepass", PASSWORD, "-file",
+ nativeCall("keytool", "-exportcert", "-alias", alias, "-keystore",
+ filename, "-storepass", password, "-file",
"littleproxy_cert");
}
@@ -103,15 +107,12 @@ private void initializeSSLContext() {
}
try {
- final KeyStore ks = KeyStore.getInstance("JKS");
- // ks.load(new FileInputStream("keystore.jks"),
- // "changeit".toCharArray());
- ks.load(new FileInputStream(keyStoreFile), PASSWORD.toCharArray());
+ final KeyStore ks = loadKeyStore();
// Set up key manager factory to use our key store
final KeyManagerFactory kmf =
KeyManagerFactory.getInstance(algorithm);
- kmf.init(ks, PASSWORD.toCharArray());
+ kmf.init(ks, password.toCharArray());
// Set up a trust manager factory to use our key store
TrustManagerFactory tmf = TrustManagerFactory
@@ -159,14 +160,36 @@ public X509Certificate[] getAcceptedIssuers() {
}
}
+ private KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
+ final KeyStore keyStore = KeyStore.getInstance("JKS");
+ URL resourceUrl = getClass().getResource(keyStoreFile);
+ if(resourceUrl != null) {
+ loadKeyStore(keyStore, resourceUrl);
+ } else {
+ File keyStoreLocalFile = new File(keyStoreFile);
+ if(!keyStoreLocalFile.isFile()) {
+ initializeKeyStore(keyStoreLocalFile.getName());
+ }
+ loadKeyStore(keyStore, keyStoreLocalFile.toURI().toURL());
+ }
+ return keyStore;
+ }
+
+ private void loadKeyStore(KeyStore keyStore, URL url) throws IOException, GeneralSecurityException {
+ try(InputStream is = url.openStream()) {
+ keyStore.load(is, password.toCharArray());
+ }
+ }
+
private String nativeCall(final String... commands) {
LOG.info("Running '{}'", Arrays.asList(commands));
final ProcessBuilder pb = new ProcessBuilder(commands);
try {
final Process process = pb.start();
- final InputStream is = process.getInputStream();
-
- byte[] data = ByteStreams.toByteArray(is);
+ byte[] data;
+ try (InputStream is = process.getInputStream()) {
+ data = ByteStreams.toByteArray(is);
+ }
String dataAsString = new String(data);
LOG.info("Completed native call: '{}'\nResponse: '" + dataAsString + "'",
diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java b/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java
new file mode 100644
index 000000000..aa47c2769
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java
@@ -0,0 +1,35 @@
+package org.littleshoot.proxy.impl;
+
+import java.net.InetSocketAddress;
+
+/**
+ * Contains information about the client.
+ */
+public class ClientDetails {
+
+ /**
+ * The user name that was used for authentication, or null if authentication wasn't performed.
+ */
+ private volatile String userName;
+
+ /**
+ * The client's address
+ */
+ private volatile InetSocketAddress clientAddress;
+
+ public String getUserName() {
+ return userName;
+ }
+
+ void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public InetSocketAddress getClientAddress() {
+ return clientAddress;
+ }
+
+ void setClientAddress(InetSocketAddress clientAddress) {
+ this.clientAddress = clientAddress;
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
index 964858fbf..a7326fef0 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
@@ -5,6 +5,8 @@
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
@@ -102,6 +104,11 @@ public class ClientToProxyConnection extends ProxyConnection {
private final AtomicInteger numberOfCurrentlyConnectingServers = new AtomicInteger(
0);
+ /**
+ * Keep track of proxy protocol header
+ */
+ private HAProxyMessage haProxyMessage = null;
+
/**
* Keep track of how many servers are currently connected.
*/
@@ -141,6 +148,8 @@ public class ClientToProxyConnection extends ProxyConnection {
*/
private volatile HttpRequest currentRequest;
+ private final ClientDetails clientDetails = new ClientDetails();
+
ClientToProxyConnection(
final DefaultHttpProxyServer proxyServer,
SslEngineSource sslEngineSource,
@@ -174,6 +183,11 @@ public void operationComplete(
LOG.debug("Created ClientToProxyConnection");
}
+ @Override
+ protected void readHAProxyMessage(HAProxyMessage msg) {
+ haProxyMessage = msg;
+ }
+
/***************************************************************************
* Reading
**************************************************************************/
@@ -783,6 +797,9 @@ private void initChannelPipeline(ChannelPipeline pipeline) {
pipeline.addLast("bytesWrittenMonitor", bytesWrittenMonitor);
pipeline.addLast("encoder", new HttpResponseEncoder());
+ if (isAcceptProxyProtocol()) {
+ pipeline.addLast("proxy-protocol-decoder", new HAProxyMessageDecoder());
+ }
// We want to allow longer request lines, headers, and chunks
// respectively.
pipeline.addLast("decoder", new HttpRequestDecoder(
@@ -808,6 +825,22 @@ private void initChannelPipeline(ChannelPipeline pipeline) {
pipeline.addLast("handler", this);
}
+ /**
+ * Is the proxy server set to accept a proxy protocol header
+ * @return True if the proxy server set to accept a proxy protocol header. False otherwise
+ */
+ boolean isAcceptProxyProtocol() {
+ return proxyServer.isAcceptProxyProtocol();
+ }
+
+ /**
+ * Is the proxy server set to send a proxy protocol header
+ * @return True if the proxy server set to send a proxy protocol header. False otherwise
+ */
+ boolean isSendProxyProtocol() {
+ return proxyServer.isSendProxyProtocol();
+ }
+
/**
* This method takes care of closing client to proxy and/or proxy to server
* connections after finishing a write.
@@ -994,6 +1027,7 @@ private boolean authenticationRequired(HttpRequest request) {
writeAuthenticationRequired(authenticator.getRealm());
return true;
}
+ clientDetails.setUserName(userName);
LOG.debug("Got proxy authorization!");
// We need to remove the header before sending the request on.
@@ -1402,6 +1436,7 @@ protected void responseWritten(HttpResponse httpResponse) {
private void recordClientConnected() {
try {
InetSocketAddress clientAddress = getClientAddress();
+ clientDetails.setClientAddress(clientAddress);
for (ActivityTracker tracker : proxyServer
.getActivityTrackers()) {
tracker.clientConnected(clientAddress);
@@ -1452,4 +1487,12 @@ private FlowContext flowContext() {
}
}
+ public HAProxyMessage getHaProxyMessage() {
+ return haProxyMessage;
+ }
+
+ public ClientDetails getClientDetails() {
+ return clientDetails;
+ }
+
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
index 23a65f08e..6a667edd6 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
@@ -1,10 +1,15 @@
package org.littleshoot.proxy.impl;
+import io.netty.handler.codec.haproxy.HAProxyCommand;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+import io.netty.handler.codec.haproxy.HAProxyProtocolVersion;
+import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
-
+import java.net.InetSocketAddress;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
+import org.littleshoot.proxy.extras.ProxyProtocolMessage;
/**
* Coordinates the various steps involved in establishing a connection, such as
@@ -166,10 +171,28 @@ void succeed() {
serverConnection.getLOG().debug(
"Connection flow completed successfully: {}", currentStep);
serverConnection.connectionSucceeded(!suppressInitialRequest);
+ relayProxyInformation();
notifyThreadsWaitingForConnection();
}
}
+ private void relayProxyInformation() {
+ if (clientConnection.isSendProxyProtocol()) {
+ ProxyProtocolMessage proxyProtocolMessage = getHAProxyMessage(clientConnection.getClientAddress(), serverConnection.getRemoteAddress());
+ if ( proxyProtocolMessage != null ){
+ serverConnection.writeToChannel(proxyProtocolMessage);
+ }
+ }
+ }
+
+ private ProxyProtocolMessage getHAProxyMessage(InetSocketAddress clientAddress, InetSocketAddress remoteAddress) {
+ HAProxyMessage haProxyMessage = clientConnection.getHaProxyMessage();
+ if ( haProxyMessage != null ){
+ return new ProxyProtocolMessage(haProxyMessage);
+ }
+ return new ProxyProtocolMessage(HAProxyProtocolVersion.V1, HAProxyCommand.PROXY, HAProxyProxiedProtocol.TCP4, clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(), clientAddress.getPort(), remoteAddress.getPort());
+ }
+
/**
* Called when the flow fails at some {@link ConnectionFlowStep}.
* Disconnects the {@link ProxyToServerConnection} and informs the
diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
index 1891532e4..2b64d1ac0 100644
--- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
+++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
@@ -117,6 +117,8 @@ public class DefaultHttpProxyServer implements HttpProxyServer {
private final int maxHeaderSize;
private final int maxChunkSize;
private final boolean allowRequestsToOriginServer;
+ private final boolean acceptProxyProtocol;
+ private final boolean sendProxyProtocol;
/**
* The alias or pseudonym for this proxy, used when adding the Via header.
@@ -230,6 +232,8 @@ public static HttpProxyServerBootstrap bootstrapFromFile(String path) {
* @param maxChunkSize
* @param allowRequestsToOriginServer
* when true, allow the proxy to handle requests that contain an origin-form URI, as defined in RFC 7230 5.3.1
+ * @param acceptProxyProtocol when true, the proxy will accept a proxy protocol header from client
+ * @param sendProxyProtocol when true, the proxy will send a proxy protocol header to the server
*/
private DefaultHttpProxyServer(ServerGroup serverGroup,
TransportProtocol transportProtocol,
@@ -252,7 +256,9 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
int maxInitialLineLength,
int maxHeaderSize,
int maxChunkSize,
- boolean allowRequestsToOriginServer) {
+ boolean allowRequestsToOriginServer,
+ boolean acceptProxyProtocol,
+ boolean sendProxyProtocol) {
this.serverGroup = serverGroup;
this.transportProtocol = transportProtocol;
this.requestedAddress = requestedAddress;
@@ -291,6 +297,8 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
this.maxHeaderSize = maxHeaderSize;
this.maxChunkSize = maxChunkSize;
this.allowRequestsToOriginServer = allowRequestsToOriginServer;
+ this.acceptProxyProtocol = acceptProxyProtocol;
+ this.sendProxyProtocol = sendProxyProtocol;
}
/**
@@ -384,6 +392,14 @@ public boolean isAllowRequestsToOriginServer() {
return allowRequestsToOriginServer;
}
+ public boolean isAcceptProxyProtocol() {
+ return acceptProxyProtocol;
+ }
+
+ public boolean isSendProxyProtocol() {
+ return sendProxyProtocol;
+ }
+
@Override
public HttpProxyServerBootstrap clone() {
return new DefaultHttpProxyServerBootstrap(serverGroup,
@@ -624,6 +640,8 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB
private int maxHeaderSize = MAX_HEADER_SIZE_DEFAULT;
private int maxChunkSize = MAX_CHUNK_SIZE_DEFAULT;
private boolean allowRequestToOriginServer = false;
+ private boolean acceptProxyProtocol = false;
+ private boolean sendProxyProtocol = false;
private DefaultHttpProxyServerBootstrap() {
}
@@ -873,6 +891,18 @@ public HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequ
return this;
}
+ @Override
+ public HttpProxyServerBootstrap withAcceptProxyProtocol(boolean acceptProxyProtocol) {
+ this.acceptProxyProtocol = acceptProxyProtocol;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withSendProxyProtocol(boolean sendProxyProtocol) {
+ this.sendProxyProtocol = sendProxyProtocol;
+ return this;
+ }
+
@Override
public HttpProxyServer start() {
return build().start();
@@ -904,7 +934,7 @@ transportProtocol, determineListenAddress(),
idleConnectionTimeout, activityTrackers, connectTimeout,
serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
- allowRequestToOriginServer);
+ allowRequestToOriginServer, acceptProxyProtocol, sendProxyProtocol);
}
private InetSocketAddress determineListenAddress() {
diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
index 58c3eb240..f68797b39 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
@@ -3,6 +3,7 @@
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateEvent;
@@ -115,12 +116,21 @@ protected void read(Object msg) {
if (tunneling) {
// In tunneling mode, this connection is simply shoveling bytes
readRaw((ByteBuf) msg);
+ } else if ( msg instanceof HAProxyMessage) {
+ readHAProxyMessage((HAProxyMessage)msg);
} else {
// If not tunneling, then we are always dealing with HttpObjects.
readHTTP((HttpObject) msg);
}
}
+ /**
+ * Read an {@link HAProxyMessage}
+ * @param msg {@link HAProxyMessage}
+ */
+ protected abstract void readHAProxyMessage(HAProxyMessage msg);
+
+
/**
* Handles reading {@link HttpObject}s.
*
diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
index b612f5160..0e5bf86a5 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
@@ -13,6 +13,7 @@
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.udt.nio.NioUdtProvider;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
@@ -50,6 +51,7 @@
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.RejectedExecutionException;
+import org.littleshoot.proxy.extras.HAProxyMessageEncoder;
import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_CHUNK;
import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_CONNECT_OK;
@@ -161,7 +163,7 @@ static ProxyToServerConnection create(DefaultHttpProxyServer proxyServer,
.getChainProxyManager();
if (chainedProxyManager != null) {
chainedProxyManager.lookupChainedProxies(initialHttpRequest,
- chainedProxies);
+ chainedProxies, clientConnection.getClientDetails());
if (chainedProxies.size() == 0) {
// ChainedProxyManager returned no proxies, can't connect
return null;
@@ -215,6 +217,12 @@ protected void read(Object msg) {
}
}
+ @Override
+ protected void readHAProxyMessage(HAProxyMessage msg) {
+ // NO-OP,
+ // We never expect server to send a proxy protocol message.
+ }
+
@Override
protected ConnectionState readHTTPInitial(HttpResponse httpResponse) {
LOG.debug("Received raw response: {}", httpResponse);
@@ -876,6 +884,9 @@ private void initChannelPipeline(ChannelPipeline pipeline,
pipeline.addLast("bytesReadMonitor", bytesReadMonitor);
pipeline.addLast("bytesWrittenMonitor", bytesWrittenMonitor);
+ if ( proxyServer.isSendProxyProtocol()) {
+ pipeline.addLast("proxy-protocol-encoder", new HAProxyMessageEncoder());
+ }
pipeline.addLast("encoder", new HttpRequestEncoder());
pipeline.addLast("decoder", new HeadAwareHttpResponseDecoder(
proxyServer.getMaxInitialLineLength(),
diff --git a/src/test/java/org/littleshoot/proxy/AuthenticatingProxyWithChainingTest.java b/src/test/java/org/littleshoot/proxy/AuthenticatingProxyWithChainingTest.java
new file mode 100644
index 000000000..1d7dfb159
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/AuthenticatingProxyWithChainingTest.java
@@ -0,0 +1,63 @@
+package org.littleshoot.proxy;
+
+import io.netty.handler.codec.http.HttpRequest;
+import org.junit.Assert;
+import org.littleshoot.proxy.impl.ClientDetails;
+
+import java.util.Queue;
+
+/**
+ * Tests a single proxy that requires username/password authentication.
+ */
+public class AuthenticatingProxyWithChainingTest extends BaseProxyTest
+ implements ProxyAuthenticator, ChainedProxyManager {
+
+ private ClientDetails savedClientDetails;
+
+ @Override
+ protected void setUp() {
+ this.proxyServer = bootstrapProxy()
+ .withPort(0)
+ .withProxyAuthenticator(this)
+ .withChainProxyManager(this)
+ .start();
+ }
+
+ @Override
+ protected String getUsername() {
+ return "user1";
+ }
+
+ @Override
+ protected String getPassword() {
+ return "user2";
+ }
+
+ @Override
+ public boolean authenticate(String userName, String password) {
+ return getUsername().equals(userName) && getPassword().equals(password);
+ }
+
+ @Override
+ protected boolean isAuthenticating() {
+ return true;
+ }
+
+ @Override
+ public String getRealm() {
+ return null;
+ }
+
+ @Override
+ public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies, ClientDetails clientDetails) {
+ savedClientDetails = clientDetails;
+ chainedProxies.add(ChainedProxyAdapter.FALLBACK_TO_DIRECT_CONNECTION);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ Assert.assertEquals(getUsername(), savedClientDetails.getUserName());
+ Assert.assertTrue(savedClientDetails.getClientAddress().getAddress().isLoopbackAddress());
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/BaseChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/BaseChainedProxyTest.java
index bb327314f..a8f9cd761 100644
--- a/src/test/java/org/littleshoot/proxy/BaseChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/BaseChainedProxyTest.java
@@ -1,6 +1,7 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpRequest;
+import org.littleshoot.proxy.impl.ClientDetails;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import java.net.InetAddress;
@@ -70,7 +71,8 @@ protected ChainedProxyManager chainedProxyManager() {
return new ChainedProxyManager() {
@Override
public void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies) {
+ Queue chainedProxies,
+ ClientDetails clientDetails) {
chainedProxies.add(newChainedProxy());
}
};
diff --git a/src/test/java/org/littleshoot/proxy/BaseProxyTest.java b/src/test/java/org/littleshoot/proxy/BaseProxyTest.java
index b6af36b47..7e052d88c 100644
--- a/src/test/java/org/littleshoot/proxy/BaseProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/BaseProxyTest.java
@@ -48,8 +48,11 @@ public void testHeadRequestFollowedByGet() throws Exception {
@Test
public void testProxyWithBadAddress()
throws Exception {
+ // This test used to try connecting to "test.localhost" and that worked for for local builds, but resulted in
+ // the wrong error (405 instead of 502) on the build server due to nginx. So, switched it to localhost:17,
+ // which should work as long as there's not a web server running on the QOTD port.
ResponseInfo response =
- httpPostWithApacheClient(new HttpHost("test.localhost"),
+ httpPostWithApacheClient(new HttpHost("localhost", 17),
DEFAULT_RESOURCE, true);
assertReceivedBadGateway(response);
}
diff --git a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java
index 8cb7efb8a..04d7c5381 100644
--- a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java
+++ b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java
@@ -9,6 +9,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Assert;
+import org.littleshoot.proxy.impl.ClientDetails;
/**
* Tests a proxy chained to a missing downstream proxy. When the downstream
@@ -27,7 +28,8 @@ protected void setUp() {
.withChainProxyManager(new ChainedProxyManager() {
@Override
public void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies) {
+ Queue chainedProxies,
+ ClientDetails clientDetails) {
chainedProxies.add(new ChainedProxyAdapter() {
@Override
public InetSocketAddress getChainedProxyAddress() {
diff --git a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToDirectDueToSSLTest.java b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToDirectDueToSSLTest.java
index b01296eaa..92359acd5 100644
--- a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToDirectDueToSSLTest.java
+++ b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToDirectDueToSSLTest.java
@@ -1,6 +1,7 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpRequest;
+import org.littleshoot.proxy.impl.ClientDetails;
import java.util.Queue;
@@ -27,7 +28,8 @@ protected ChainedProxyManager chainedProxyManager() {
return new ChainedProxyManager() {
@Override
public void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies) {
+ Queue chainedProxies,
+ ClientDetails clientDetails) {
// This first one has a bad cert
chainedProxies.add(newChainedProxy());
chainedProxies
diff --git a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java
index c16ffa9d1..f2df4f4fb 100644
--- a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java
+++ b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java
@@ -1,6 +1,7 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpRequest;
+import org.littleshoot.proxy.impl.ClientDetails;
import java.util.Queue;
@@ -22,7 +23,8 @@ protected ChainedProxyManager chainedProxyManager() {
return new ChainedProxyManager() {
@Override
public void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies) {
+ Queue chainedProxies,
+ ClientDetails clientDetails) {
// This first one has a bad cert
chainedProxies.add(newChainedProxy());
// This 2nd one should work
diff --git a/src/test/java/org/littleshoot/proxy/HttpFilterTest.java b/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
index 56e3a229e..d50e8f365 100644
--- a/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
+++ b/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
@@ -14,6 +14,7 @@
import org.junit.Before;
import org.junit.Test;
import org.littleshoot.proxy.extras.SelfSignedSslEngineSource;
+import org.littleshoot.proxy.impl.ClientDetails;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import org.littleshoot.proxy.test.HttpClientUtil;
import org.mockserver.integration.ClientAndServer;
@@ -548,7 +549,7 @@ public HttpFilters filterRequest(HttpRequest originalRequest) {
.withFiltersSource(filtersSource)
.withChainProxyManager(new ChainedProxyManager() {
@Override
- public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies) {
+ public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies, ClientDetails clientDetails) {
chainedProxies.add(new ChainedProxyAdapter() {
@Override
public InetSocketAddress getChainedProxyAddress() {
@@ -616,7 +617,7 @@ public HttpFilters filterRequest(HttpRequest originalRequest) {
.withFiltersSource(filtersSource)
.withChainProxyManager(new ChainedProxyManager() {
@Override
- public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies) {
+ public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies, ClientDetails clientDetails) {
chainedProxies.add(new ChainedProxyAdapter() {
@Override
public InetSocketAddress getChainedProxyAddress() {
diff --git a/src/test/java/org/littleshoot/proxy/NoChainedProxiesTest.java b/src/test/java/org/littleshoot/proxy/NoChainedProxiesTest.java
index ec152e13e..3eef7c952 100644
--- a/src/test/java/org/littleshoot/proxy/NoChainedProxiesTest.java
+++ b/src/test/java/org/littleshoot/proxy/NoChainedProxiesTest.java
@@ -5,6 +5,7 @@
import java.util.Queue;
import org.junit.Test;
+import org.littleshoot.proxy.impl.ClientDetails;
/**
* Tests that when there are no chained proxies, we get a bad gateway.
@@ -17,7 +18,8 @@ protected void setUp() {
.withChainProxyManager(new ChainedProxyManager() {
@Override
public void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies) {
+ Queue chainedProxies,
+ ClientDetails clientDetails) {
// Leave list empty
}
})
diff --git a/src/test/java/org/littleshoot/proxy/SelfSignedSslEngineChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/SelfSignedSslEngineChainedProxyTest.java
new file mode 100644
index 000000000..727a69161
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/SelfSignedSslEngineChainedProxyTest.java
@@ -0,0 +1,34 @@
+package org.littleshoot.proxy;
+
+import org.littleshoot.proxy.*;
+import org.littleshoot.proxy.extras.SelfSignedSslEngineSource;
+
+import javax.net.ssl.SSLEngine;
+
+import static org.littleshoot.proxy.TransportProtocol.TCP;
+
+public class SelfSignedSslEngineChainedProxyTest extends BaseChainedProxyTest {
+ private final SslEngineSource sslEngineSource = new SelfSignedSslEngineSource("/certificate/chain_proxy_keystore.jks",
+ false, true, "littleproxy", "Be Your Own Lantern");
+
+ @Override
+ protected HttpProxyServerBootstrap upstreamProxy() {
+ return super.upstreamProxy()
+ .withSslEngineSource(sslEngineSource);
+ }
+
+ @Override
+ protected ChainedProxy newChainedProxy() {
+ return new BaseChainedProxy() {
+ @Override
+ public boolean requiresEncryption() {
+ return true;
+ }
+
+ @Override
+ public SSLEngine newSslEngine() {
+ return sslEngineSource.newSslEngine();
+ }
+ };
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/TestUtils.java b/src/test/java/org/littleshoot/proxy/TestUtils.java
index 61cd3fce2..af9d4ec32 100644
--- a/src/test/java/org/littleshoot/proxy/TestUtils.java
+++ b/src/test/java/org/littleshoot/proxy/TestUtils.java
@@ -88,10 +88,10 @@ public void handle(String target, Request baseRequest,
}
long numberOfBytesRead = 0;
- InputStream in = new BufferedInputStream(request
- .getInputStream());
- while (in.read() != -1) {
- numberOfBytesRead += 1;
+ try (InputStream in = new BufferedInputStream(request.getInputStream())) {
+ while (in.read() != -1) {
+ numberOfBytesRead += 1;
+ }
}
System.out.println("Done reading # of bytes: "
+ numberOfBytesRead);
@@ -163,10 +163,10 @@ public void handle(String target, Request baseRequest,
}
long numberOfBytesRead = 0;
- InputStream in = new BufferedInputStream(request
- .getInputStream());
- while (in.read() != -1) {
- numberOfBytesRead += 1;
+ try (InputStream in = new BufferedInputStream(request.getInputStream())) {
+ while (in.read() != -1) {
+ numberOfBytesRead += 1;
+ }
}
System.out.println("Done reading # of bytes: "
+ numberOfBytesRead);
diff --git a/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java b/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java
index 26f330c1c..5452ad8a1 100644
--- a/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java
+++ b/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java
@@ -93,22 +93,19 @@ public int available() throws IOException {
final long cl = entity.getContentLength();
assertEquals(CONTENT_LENGTH, cl);
- InputStream content = entity.getContent();
- if (!slowServer) {
- content = new ThrottledInputStream(entity.getContent(), 10 * 1000);
- }
- final byte[] input = new byte[100000];
- int read = content.read(input);
-
int bytesRead = 0;
- while (read != -1) {
- bytesRead += read;
- read = content.read(input);
+ try (InputStream content = slowServer ? new ThrottledInputStream(entity.getContent(), 10 * 1000) : entity.getContent()) {
+ final byte[] input = new byte[100000];
+ int read = content.read(input);
+
+ while (read != -1) {
+ bytesRead += read;
+ read = content.read(input);
+ }
}
assertEquals(CONTENT_LENGTH, bytesRead);
// final String body = IOUtils.toString(entity.getContent());
EntityUtils.consume(entity);
- content.close();
System.out
.println("------------------ Memory Usage At Beginning ------------------");
TestUtils.getOpenFileDescriptorsAndPrintMemoryUsage();
@@ -138,39 +135,37 @@ private void startServerOnThread(int port, boolean slowReader)
try {
server.setSoTimeout(100000);
final Socket sock = server.accept();
- InputStream is = sock.getInputStream();
- if (slowReader) {
- is = new ThrottledInputStream(is, 10 * 1000);
- }
- BufferedReader br = new BufferedReader(new InputStreamReader(is));
- while (br.read() != 0) {
+ try (InputStream is = slowReader ? new ThrottledInputStream(sock.getInputStream(), 10 * 1000) : sock.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
+ while (br.read() != 0) {
+ }
}
- final OutputStream os = sock.getOutputStream();
- final String responseHeaders =
- "HTTP/1.1 200 OK\r\n" +
- "Date: Sun, 20 Jan 2013 00:16:23 GMT\r\n" +
- "Expires: -1\r\n" +
- "Cache-Control: private, max-age=0\r\n" +
- "Content-Type: text/html; charset=ISO-8859-1\r\n" +
- "Server: gws\r\n" +
- "Content-Length: " + CONTENT_LENGTH + "\r\n\r\n"; // 10
- // gigs
- // or
- // so.
-
- os.write(responseHeaders.getBytes(Charset.forName("UTF-8")));
-
- int bufferSize = 100000;
- final byte[] bytes = new byte[bufferSize];
- Arrays.fill(bytes, (byte) 77);
- int remainingBytes = CONTENT_LENGTH;
-
- while (remainingBytes > 0) {
- int numberOfBytesToWrite = Math.min(remainingBytes, bufferSize);
- os.write(bytes, 0, numberOfBytesToWrite);
- remainingBytes -= numberOfBytesToWrite;
+ try (OutputStream os = sock.getOutputStream()) {
+ final String responseHeaders =
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Sun, 20 Jan 2013 00:16:23 GMT\r\n" +
+ "Expires: -1\r\n" +
+ "Cache-Control: private, max-age=0\r\n" +
+ "Content-Type: text/html; charset=ISO-8859-1\r\n" +
+ "Server: gws\r\n" +
+ "Content-Length: " + CONTENT_LENGTH + "\r\n\r\n"; // 10
+ // gigs
+ // or
+ // so.
+
+ os.write(responseHeaders.getBytes(Charset.forName("UTF-8")));
+
+ int bufferSize = 100000;
+ final byte[] bytes = new byte[bufferSize];
+ Arrays.fill(bytes, (byte) 77);
+ int remainingBytes = CONTENT_LENGTH;
+
+ while (remainingBytes > 0) {
+ int numberOfBytesToWrite = Math.min(remainingBytes, bufferSize);
+ os.write(bytes, 0, numberOfBytesToWrite);
+ remainingBytes -= numberOfBytesToWrite;
+ }
}
- os.close();
} finally {
server.close();
}
diff --git a/src/test/java/org/littleshoot/proxy/extras/SelfSignedMitmManagerTest.java b/src/test/java/org/littleshoot/proxy/extras/SelfSignedMitmManagerTest.java
new file mode 100644
index 000000000..9b6f8ebc2
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/extras/SelfSignedMitmManagerTest.java
@@ -0,0 +1,47 @@
+package org.littleshoot.proxy.extras;
+
+import io.netty.handler.codec.http.HttpRequest;
+import org.junit.Test;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.junit.Assert.assertEquals;
+
+public class SelfSignedMitmManagerTest {
+
+ @Test
+ public void testServerSslEnginePeerAndPort() {
+ String peer = "localhost";
+ int port = 8090;
+ SelfSignedSslEngineSource source = mock(SelfSignedSslEngineSource.class);
+ SelfSignedMitmManager manager = new SelfSignedMitmManager(source);
+ SSLEngine engine = mock(SSLEngine.class);
+ when(source.newSslEngine(peer, port)).thenReturn(engine);
+ assertEquals(engine, manager.serverSslEngine(peer, port));
+ }
+
+ @Test
+ public void testServerSslEngine() {
+ SelfSignedSslEngineSource source = mock(SelfSignedSslEngineSource.class);
+ SelfSignedMitmManager manager = new SelfSignedMitmManager(source);
+ SSLEngine engine = mock(SSLEngine.class);
+ when(source.newSslEngine()).thenReturn(engine);
+ assertEquals(engine, manager.serverSslEngine());
+ }
+
+ @Test
+ public void testClientSslEngineFor() {
+ HttpRequest request = mock(HttpRequest.class);
+ SSLSession session = mock(SSLSession.class);
+ SelfSignedSslEngineSource source = mock(SelfSignedSslEngineSource.class);
+ SelfSignedMitmManager manager = new SelfSignedMitmManager(source);
+ SSLEngine engine = mock(SSLEngine.class);
+ when(source.newSslEngine()).thenReturn(engine);
+ assertEquals(engine, manager.clientSslEngineFor(request, session));
+ verifyZeroInteractions(request, session);
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/haproxy/BaseProxyProtocolTest.java b/src/test/java/org/littleshoot/proxy/haproxy/BaseProxyProtocolTest.java
new file mode 100644
index 000000000..29d416a7b
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/haproxy/BaseProxyProtocolTest.java
@@ -0,0 +1,142 @@
+package org.littleshoot.proxy.haproxy;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ChannelFactory;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpRequestEncoder;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import java.net.InetSocketAddress;
+import org.junit.After;
+import org.littleshoot.proxy.HttpProxyServer;
+import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
+
+/**
+ * Base for running Proxy protocol tests.
+ * Proxy Protocol tests need special client and servers that are
+ * capable of emitting and consuming proxy protocol headers.
+ */
+public abstract class BaseProxyProtocolTest {
+
+ private EventLoopGroup childGroup;
+ private EventLoopGroup parentGroup;
+ private EventLoopGroup clientWorkGroup;
+ private ProxyProtocolServerHandler proxyProtocolServerHandler;
+ private HttpProxyServer proxyServer;
+ private int proxyPort;
+ private boolean acceptProxy = true;
+ private boolean sendProxy = true;
+ int serverPort;
+ static final String SOURCE_ADDRESS = "192.168.0.153";
+ static final String DESTINATION_ADDRESS = "192.168.0.154";
+ static final String SOURCE_PORT = "123";
+ static final String DESTINATION_PORT = "456";
+
+
+ public void setup(boolean acceptProxy, boolean sendProxy) throws Exception {
+ this.acceptProxy = acceptProxy;
+ this.sendProxy = sendProxy;
+ startProxyServer();
+ startServer();
+ startClient();
+ }
+
+ void startServer() {
+ parentGroup = new NioEventLoopGroup();
+ childGroup = new NioEventLoopGroup();
+ ServerBootstrap b = new ServerBootstrap();
+ b.group(parentGroup, childGroup)
+ .channelFactory(new ChannelFactory() {
+ public ServerChannel newChannel() {
+ return new NioServerSocketChannel();
+ }
+ })
+ .childHandler(new ChannelInitializer() {
+ @Override
+ public void initChannel(SocketChannel ch) {
+ proxyProtocolServerHandler = new ProxyProtocolServerHandler();
+ ch.pipeline().addLast(new HAProxyMessageDecoder()).addLast(new HttpRequestDecoder()).addLast(proxyProtocolServerHandler);
+ }
+ }).option(ChannelOption.SO_BACKLOG, 128)
+ .childOption(ChannelOption.SO_KEEPALIVE, true);
+
+ ChannelFuture f = b.bind(0)
+ .awaitUninterruptibly();
+ Throwable cause = f.cause();
+ if (cause != null) {
+ throw new RuntimeException(cause);
+ }
+ serverPort = ((InetSocketAddress)f.channel().localAddress()).getPort();
+ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+ public void run() {
+ stopServer();
+ }
+ }, "stopServerHook"));
+ }
+
+ void startClient() throws Exception {
+ String host = "localhost";
+ clientWorkGroup = new NioEventLoopGroup();
+ Bootstrap b = new Bootstrap();
+ b.group(clientWorkGroup);
+ b.channel(NioSocketChannel.class);
+ b.option(ChannelOption.SO_KEEPALIVE, true);
+ b.handler(new ChannelInitializer() {
+ @Override
+ public void initChannel(SocketChannel ch) {
+ ch.pipeline().addLast(new ReadTimeoutHandler(1));
+ if (acceptProxy) {
+ ch.pipeline().addLast(new ProxyProtocolTestEncoder());
+ }
+ ch.pipeline().addLast(new HttpRequestEncoder()).addLast(new ProxyProtocolClientHandler(serverPort, getProxyProtocolHeader()));
+ }
+ });
+ ChannelFuture f = b.connect(host, proxyPort).sync();
+ f.channel().closeFuture().sync();
+ }
+
+ HAProxyMessage getRelayedHaProxyMessage() {
+ return proxyProtocolServerHandler.getHaProxyMessage();
+ }
+
+ private void stopServer() {
+ childGroup.shutdownGracefully();
+ parentGroup.shutdownGracefully();
+ }
+
+ private void stopProxyServer() {
+ proxyServer.abort();
+ }
+
+ private void startProxyServer() {
+ proxyServer = DefaultHttpProxyServer.bootstrap()
+ .withPort(0)
+ .withAcceptProxyProtocol(acceptProxy)
+ .withSendProxyProtocol(sendProxy)
+ .start();
+ proxyPort = proxyServer.getListenAddress().getPort();
+
+ }
+
+ private ProxyProtocolHeader getProxyProtocolHeader() {
+ return new ProxyProtocolHeader(SOURCE_ADDRESS, DESTINATION_ADDRESS, SOURCE_PORT, DESTINATION_PORT);
+ }
+
+ @After
+ public void tearDown() {
+ stopServer();
+ stopProxyServer();
+ }
+
+}
diff --git a/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolClientHandler.java b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolClientHandler.java
new file mode 100644
index 000000000..288439bfb
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolClientHandler.java
@@ -0,0 +1,37 @@
+package org.littleshoot.proxy.haproxy;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultHttpRequest;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpVersion;
+
+
+public class ProxyProtocolClientHandler extends ChannelInboundHandlerAdapter {
+
+ private static final String HOST = "http://localhost";
+ private int serverPort;
+ private ProxyProtocolHeader proxyProtocolHeader;
+
+ ProxyProtocolClientHandler(int serverPort, ProxyProtocolHeader proxyProtocolHeader) {
+ this.serverPort = serverPort;
+ this.proxyProtocolHeader = proxyProtocolHeader;
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ ctx.write(getHAProxyHeader());
+ ctx.writeAndFlush(getConnectRequest());
+ }
+
+ private HttpRequest getConnectRequest() {
+ return new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, HOST + ":" + serverPort);
+ }
+
+
+ private String getHAProxyHeader() {
+ return String.format("PROXY TCP4 %s %s %s %s\r\n", proxyProtocolHeader.getSourceAddress(), proxyProtocolHeader.getDestinationAddress(),
+ proxyProtocolHeader.getSourcePort(), proxyProtocolHeader.getDestinationPort());
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolHeader.java b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolHeader.java
new file mode 100644
index 000000000..d989139b1
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolHeader.java
@@ -0,0 +1,33 @@
+package org.littleshoot.proxy.haproxy;
+
+class ProxyProtocolHeader {
+
+ private String sourceAddress;
+ private String destinationAddress;
+ private String sourcePort;
+ private String destinationPort;
+
+ ProxyProtocolHeader(String sourceAddress, String destinationAddress, String sourcePort, String destinationPort) {
+ this.sourceAddress = sourceAddress;
+ this.destinationAddress = destinationAddress;
+ this.sourcePort = sourcePort;
+ this.destinationPort = destinationPort;
+ }
+
+ String getSourceAddress() {
+ return sourceAddress;
+ }
+
+ String getDestinationAddress() {
+ return destinationAddress;
+ }
+
+ String getSourcePort() {
+ return sourcePort;
+ }
+
+ String getDestinationPort() {
+ return destinationPort;
+ }
+
+}
diff --git a/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolServerHandler.java b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolServerHandler.java
new file mode 100644
index 000000000..eec2e72d9
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolServerHandler.java
@@ -0,0 +1,21 @@
+package org.littleshoot.proxy.haproxy;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+
+public class ProxyProtocolServerHandler extends ChannelInboundHandlerAdapter {
+
+ private HAProxyMessage haProxyMessage;
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if ( msg instanceof HAProxyMessage){
+ this.haProxyMessage = (HAProxyMessage) msg;
+ }
+ }
+
+ HAProxyMessage getHaProxyMessage() {
+ return haProxyMessage;
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolTest.java b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolTest.java
new file mode 100644
index 000000000..a0c6baee2
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolTest.java
@@ -0,0 +1,45 @@
+package org.littleshoot.proxy.haproxy;
+
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ProxyProtocolTest extends BaseProxyProtocolTest {
+
+
+ private static final String LOCALHOST = "127.0.0.1";
+ private static final boolean ACCEPT_PROXY = true;
+ private static final boolean SEND_PROXY = true;
+ private static final boolean DO_NOT_ACCEPT_PROXY = false;
+ private static final boolean DO_NOT_SEND_PROXY = false;
+
+ @Test
+ public void canRelayProxyProtocolHeader() throws Exception {
+ setup(ACCEPT_PROXY, SEND_PROXY);
+ HAProxyMessage haProxyMessage = getRelayedHaProxyMessage();
+ Assert.assertNotNull(haProxyMessage);
+ Assert.assertEquals(SOURCE_ADDRESS, haProxyMessage.sourceAddress());
+ Assert.assertEquals(DESTINATION_ADDRESS, haProxyMessage.destinationAddress());
+ Assert.assertEquals(SOURCE_PORT, String.valueOf(haProxyMessage.sourcePort()));
+ Assert.assertEquals(DESTINATION_PORT, String.valueOf(haProxyMessage.destinationPort()));
+ }
+
+ @Test
+ public void canSendProxyProtocolHeader() throws Exception {
+ setup(DO_NOT_ACCEPT_PROXY, SEND_PROXY);
+ HAProxyMessage haProxyMessage = getRelayedHaProxyMessage();
+ Assert.assertNotNull(haProxyMessage);
+ Assert.assertEquals(LOCALHOST, haProxyMessage.sourceAddress());
+ Assert.assertEquals(LOCALHOST, haProxyMessage.destinationAddress());
+ Assert.assertEquals(String.valueOf(serverPort), String.valueOf(haProxyMessage.destinationPort()));
+ }
+
+ @Test
+ public void canAcceptProxyProtocolHeader() throws Exception {
+ setup(ACCEPT_PROXY, DO_NOT_SEND_PROXY);
+ HAProxyMessage haProxyMessage = getRelayedHaProxyMessage();
+ Assert.assertNull(haProxyMessage);
+ }
+
+
+}
diff --git a/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolTestEncoder.java b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolTestEncoder.java
new file mode 100644
index 000000000..cc8bbe89c
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/haproxy/ProxyProtocolTestEncoder.java
@@ -0,0 +1,21 @@
+package org.littleshoot.proxy.haproxy;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.MessageToByteEncoder;
+
+public class ProxyProtocolTestEncoder extends MessageToByteEncoder {
+
+ @Override
+ protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {
+ out.writeBytes(msg.getBytes());
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+ super.write(ctx, msg, promise);
+ }
+
+
+}
diff --git a/src/test/resources/certificate/chain_proxy_keystore.jks b/src/test/resources/certificate/chain_proxy_keystore.jks
new file mode 100644
index 000000000..6028a70a9
Binary files /dev/null and b/src/test/resources/certificate/chain_proxy_keystore.jks differ