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 @@ -[![Build Status](https://travis-ci.org/adamfisk/LittleProxy.png?branch=master)](https://travis-ci.org/adamfisk/LittleProxy) +[![Build Status](https://travis-ci.com/mrog/LittleProxy.svg?branch=master)](https://travis-ci.com/mrog/LittleProxy) +[![DepShield Badge](https://depshield.sonatype.org/badges/mrog/LittleProxy/depshield.svg)](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