From 5d7f778cc3da4e7839ebb861492eeaf2064f5093 Mon Sep 17 00:00:00 2001 From: Ox To A Cart Date: Mon, 26 Aug 2013 19:07:32 -0500 Subject: [PATCH] Refactored connection flow for better composition and readability --- chain_proxy_keystore_1.jks | Bin 0 -> 3749 bytes littleproxy_cert | Bin 1198 -> 1233 bytes .../littleshoot/proxy/HttpProxyServer.java | 7 +- .../java/org/littleshoot/proxy/Launcher.java | 2 +- .../proxy/impl/ClientToProxyConnection.java | 213 ++++-------- .../proxy/impl/ConnectionFlow.java | 97 ++++++ .../proxy/impl/ConnectionFlowStep.java | 115 ++++++ .../proxy/impl/ConnectionState.java | 76 +++- .../proxy/impl/DefaultHttpProxyServer.java | 39 ++- .../proxy/impl/ProxyConnection.java | 137 ++++---- .../proxy/impl/ProxyToServerConnection.java | 328 ++++++++---------- .../littleshoot/proxy/ChainedProxyTest.java | 40 ++- .../proxy/ChainedProxyWithFallbackTest.java | 15 +- .../java/org/littleshoot/proxy/IdleTest.java | 5 +- .../proxy/SelfSignedSSLContextSource.java | 21 +- .../littleshoot/proxy/SimpleProxyTest.java | 6 +- .../java/org/littleshoot/proxy/TestUtils.java | 122 ------- ...ernamePasswordAuthenticatingProxyTest.java | 17 +- .../proxy/VariableSpeedClientServerTest.java | 5 +- src/test/resources/log4j.properties | 6 +- 20 files changed, 673 insertions(+), 578 deletions(-) create mode 100644 chain_proxy_keystore_1.jks create mode 100644 src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java create mode 100644 src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java diff --git a/chain_proxy_keystore_1.jks b/chain_proxy_keystore_1.jks new file mode 100644 index 0000000000000000000000000000000000000000..6028a70a9d205da5af573607a4ca79542a1e99b3 GIT binary patch literal 3749 zcmZ{mS2!CE--eTj5iM$u*sHY@sMO08!XzVJA(AuL`joMq4ma0u{ zwbe+C7PY^A@AV%1Pu_#)puYkBC)dz!SS;G900~c9?Ll*2UAHwMoMd6-H`Z+qgtOWhYw1% z91Sk%wCNiMtevawIC2L0g&FZ2LrTT#5J`fa&4<%vpH^3Hb!yS}Wa)q{sgl_TU;6_| z{G@xLrcG^Z7BwV3?jYz_s$zydH#Bz z-!fQkH6CWyX(EMw6_S}nMQHqpd^6v<;V~wbcnPXuIvwg}Ct-eU;iIxCmQ@Y_RnjUf1gA8{mr5mrz z1V_A5k7)6vCq2*9nmga!nfHBsMD|XRIq1<;A*};G3oP zR&f$F(gk~(%j(HG0pl!l=0BfpOPr1$Wh_G%if?-mc^WTw5xW(27m9pT_KDjv%-oke ziK;m6+f==ApLh`@5;XqhFQ>-92fFLi%Af!DlZ4-o8O-)1pc)Efte~AA>zq0om#j*Qh{b6#GXmvNGgZiwKV}&{ zlf^d2Dt6C-Dpm|PV9$|ym($n}Tl$32s;YYzsT%K7cqL6X`!mGcJ3sR%XcSaobGWt! z%Eu}~WE8>a9u=jY4lSJ2?pH2~em0gFz;3U~(>igl!w6M1?P#2R>%m8ZfX3<~#jXXM zrb^pP*i4dCOzBO}sHek8-y-%5VL z_lC8zxv1wb+3f^{YVfp7sHeUeK0vqG8JNHMZ&Y+r@RIJCdl3`^)5={8*{uM-(1)-)m_n+< zN4K+KfYE??*)G4_e?~3_&mB^07-LTGy5$a$TtsCrGq0(^~q*x+!a`c6^G~GPa@tN->wNiC;NYyi%3m!Z_5Ab=pD2&n$;FPtj zD$=lm)MsJ!(`JLF!_4c_iw#h0leG>pw<5|n%R`xG9c*Vh2yP;|sSaWoa<8;*ig>XR z0Lv%-`5w?=Z^r`3kr&qqz ziB|BVZdFCGvO03xvl-(u|Up z2}aEeH!sRiB)`vRJG{w{;l&22SHjkWRLnJsvYnF9=@`MQhR7YTytcC+VB*FIUZyaO z`ePrZGuq)yluKf=WIS>`t8J>Ua-vSxSQ-^%t!9JwR>~Bh7%h-V5TP3?y*=-=L&aN&XJvN9DnoxprG;UIF3+IS57z{Ff9T$FWi~qI{*W0YX zY;$MIoFy`jq{{-8;2CtR`iuRHvcXq~?~cA4(s6$H!l$hEbgxX9nw(}1p(Ohw&S*m` zo?}jqa)_~7{Ifyex-|DYe@Ep4O0xVEg&3q{gY7WcmTbLrpdFZaia{ z1>THtKIojLR%HL(fr}@nb+2&(kJR;QyW%`?6H-<_!AtRXVeL?&spDe_izc!o2)U_1 zAe)iZa`?IK*GBuzKZ>3DHtTctF-kRAPb+))bz$>kb=Rv)uwOi5)u6 zl-aKW);DrZuGiGl9K{J*8(rUis%xDoL=Dz45YXzubElG54=>NnW=(Ybs!JEXHHxx* zbZ(2ZU~wghe=1#3fHk~x*;o?|Kg>b*ySR4bsG24x)ms?r4!P$#3cU;dz~tzC-&3clg`z8%)rvkKc|J@; z!#Dax{PULal%qr%IZNy6jWi}I=&eTPr&t{z8hKE>wK#$5TKv5(L@5yDTA#Pee%S{P zxf+|rmx0Mi-+?3Ga&Y)VHacne{}(Xn{}%uA5)b14_sCxpB*TN~0Du3W z2H`1$)GK8z{raky7;4qodKNQJcbSk;ja;*g3($%3I~5r3aPBLt&3`VGDs{5)Uz zfF3#i!1T@RPGyBK@A2M=U9bCGqJ#bT%doFB9`g&oviD!$&DyRs(R%yH+3PI!s=b6O zd#D;OyF6U0gn1vaGM1@TR8A>MvipHsTX!ygQ#QAy)u{iF+>H;{(23aevPHp=leIuiC1Ujp5dx#P zS~}006;6itUY!lI+xc1TsKmwQ(0ke_loA==bCU*FbzFl>PhFaQD_bUR-oHg2L!gt%pC-s#G^GsUWh!<^Q8+@COvk6XvZa92*W5C49JrEEUMX>1SP z7;ZWI9$(X@HwSM#`NUo(&1*A|ZO+I41v1%9LnnsDGVH5?&xA!(D`*rEhZh?9>(g&n z2ARLl^73`me{kOva)xcipx07zr&K745^za0550r+%WRX4N2n5vsxjY!E!G-K)YZ?N z5cw#|H@Vu;SW@vw=S6Z4>{BHN2_WBC76W6IeJ> zfqd;Dfn%ILJcQ#)mm5~wSEX~%kE1Y2@vjQ#$4{ld9?8S?>xJ1}rVRk#lZc}^zJg}P GjsF9hOZXoE literal 0 HcmV?d00001 diff --git a/littleproxy_cert b/littleproxy_cert index e180a3ab0bddabda2053770a2a3081cb1caf8f1f..d7bd7de1cacab9834e7a54ed6c420752a451f44c 100644 GIT binary patch delta 1163 zcmV;61a$kZ3DF54FoFclFoFWLpaTK{0s;h0@5d`J4F(A+hDe6@4FLfQksu)&I5IXd zH!(OdF!VRpKqL*23XSOiQw zY< zzh5$12p{t~C)fqHMKv7H96gc6m$B}lDd`m@`%8Eg2$|dF@VyRc*LrMpH*k^VxqkIQP$bW|aVjDmp896aVsTJ`z9R>qc9S#H*1QdJ8JdN7?a?NDc zPo;IfBJ;$7FgGv_1_>&LNQU=?z0oUxPRSGJM|Jo%`#hYOAy zP8iMzY}W(;czY;iPbO*Zncth*X5YW5z-;J}qOWZNI z`NkQmG9X^fpI9Lx$_dxR4G$}9bQ1AE1p6i_KducsH}(BPOy=3A|jvMSpIURDr{yUILW5hJH`% z7D%-r8qo41HD9V}6rjW|5{<9@Ee5_>1fP~}88l9_l`oJ9|9^FhH5`E zJ8XleO8$oZ4poosc?LIMYi9pVncg2G!VMq@Y?b119bYr{D{T#8-PN=afFXy5h?ytC znbzxVpfB2aM-;hkpq} zES{)EOtl_bd!hFOE{BTDUhN`x6Uo$=H*iQ$7?xN6b9|4eSjM2n}Ml z9TbFaKhd^^l%n6|>AdPmwiaiO;LmutwOj-mOdz{^e6Y=Kg~cQH-Bjc=KqDL#6k9OX z66B6ZovJ^t+X++sV*;e4;8G_BVt;m4xMwYDKdW$@n|1Ag!{*%~J9@IU3qhI@e=N$K z&z;iboQ_f96(zO7h~@n~=mW&v@OKR@JhluZnV0KSL1g=;)KuM+O%-EObL+W9qW+c4 za(|@L^IgynO zgAS*c6QWbp+UIoz?aaB&oPc#;Y}mDXVR-#Nx)3^Nf`lciI}7*p#XwFxy<$uGU@|9# z!=%Z6BG?@9-#~bnSd$w(PJpRtP|zpRD04XFoqROmt-C9Ay+8-48hKG{uFvwtAt}b9_K${41ZW}hWUAfW$Qp2 z#Ngp)GfI7k1zdlx5iHxMm`P4Ido!2CXm@l3ZxTb+n*Xi%vs1&ruz$Rxo`!G>v+GFr zZ`es&$f2YPku6YSscQv~_DWve6R*^=RTD6Mho&m%^3w_HrynD|6_)-%JiOe~d;opj zLr7F|HGk)_-LU%>9Dg1HHw#@#1 u5F+OH>6}#L6Cs>y?e{GuSrm?-5qD5hb%nGd{i)Xs(()DYXn4}M6VfxY?H~OB diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServer.java b/src/main/java/org/littleshoot/proxy/HttpProxyServer.java index af2346b7b..c0dae7472 100644 --- a/src/main/java/org/littleshoot/proxy/HttpProxyServer.java +++ b/src/main/java/org/littleshoot/proxy/HttpProxyServer.java @@ -7,8 +7,10 @@ public interface HttpProxyServer { /** * Starts the server. + * + * @return itself */ - void start(); + HttpProxyServer start(); /** * Stops the server. @@ -25,8 +27,9 @@ public interface HttpProxyServer { * @param anyAddress * Whether or not to bind to "any" address - 0.0.0.0. This is the * default. + * @return itself */ - void start(boolean localOnly, boolean anyAddress); + HttpProxyServer start(boolean localOnly, boolean anyAddress); /** * Add an ActivityTracker for tracking proxying activity. diff --git a/src/main/java/org/littleshoot/proxy/Launcher.java b/src/main/java/org/littleshoot/proxy/Launcher.java index 7cecfb7df..9a5b858cc 100755 --- a/src/main/java/org/littleshoot/proxy/Launcher.java +++ b/src/main/java/org/littleshoot/proxy/Launcher.java @@ -100,7 +100,7 @@ public static void main(final String... args) { HttpProxyServer server = builder.build(); System.out.println("About to start..."); - server.start(); + server.start(false, true); } private static void printHelp(final Options options, diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java index 4cde0200b..d117ed064 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java @@ -5,8 +5,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; @@ -19,10 +17,8 @@ import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.GenericFutureListener; import java.io.UnsupportedEncodingException; import java.net.InetAddress; @@ -38,7 +34,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLContext; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; @@ -118,9 +114,14 @@ public class ClientToProxyConnection extends ProxyConnection { ClientToProxyConnection( DefaultHttpProxyServer proxyServer, + SSLContext sslContext, ChannelPipeline pipeline) { - super(AWAITING_INITIAL, proxyServer); + super(AWAITING_INITIAL, proxyServer, sslContext, false); initChannelPipeline(pipeline); + if (sslContext != null) { + LOG.debug("Encrypting traffic from client using SSL"); + encrypt(pipeline); + } LOG.debug("Created ClientToProxyConnection"); } @@ -154,12 +155,18 @@ protected ConnectionState readInitial(HttpRequest httpRequest) { String chainedProxyHostAndPort = getChainProxyHostAndPort(httpRequest); TransportProtocol transportProtocol = TCP; + SSLContext proxyToServerSSLContext = null; String hostAndPort = serverHostAndPort; if (chainedProxyHostAndPort != null) { hostAndPort = chainedProxyHostAndPort; transportProtocol = proxyServer.getChainProxyManager() .getTransportProtocol(); + if (proxyServer.getChainProxyManager().requiresTLSEncryption( + httpRequest)) { + proxyToServerSSLContext = proxyServer + .getChainProxyManager().getSSLContext(); + } } recordRequestReceivedFromClient(transportProtocol, @@ -186,8 +193,11 @@ protected ConnectionState readInitial(HttpRequest httpRequest) { } try { currentServerConnection = connectToServer(httpRequest, - hostAndPort, serverHostAndPort, - transportProtocol, chainedProxyHostAndPort); + transportProtocol, + proxyToServerSSLContext, + hostAndPort, + serverHostAndPort, + chainedProxyHostAndPort); } catch (UnknownHostException uhe) { LOG.info("Bad Host {}", httpRequest.getUri()); writeBadGateway(httpRequest); @@ -270,42 +280,38 @@ void respond(ProxyToServerConnection serverConnection, **************************************************************************/ /** - * While we're in the process of connecting to a server, stop reading. + * Called when {@link ProxyToServerConnection} starts its connection flow. * * @param serverConnection */ - protected void connectingToServer(ProxyToServerConnection serverConnection) { + protected void serverConnectionFlowStarted( + ProxyToServerConnection serverConnection) { stopReading(); this.numberOfCurrentlyConnectingServers.incrementAndGet(); } /** - * Once all servers have connected, resume reading. + * If the {@link ProxyToServerConnection} completes its connection lifecycle + * successfully, this method is called to let us know about it. * * @param serverConnection - * the ProxyToServerConnection that connected - * @param initialRequest - * the HttpRequest that prompted this connection - * @param connectionSuccessful - * whether or not the attempt to connect was successful */ - protected void serverConnected(ProxyToServerConnection serverConnection, - HttpRequest initialRequest, - boolean connectionSuccessful) { - LOG.debug("{} to server: {}", - connectionSuccessful ? "Finished connecting" - : "Failed to connect", serverConnection.getAddress()); - if (connectionSuccessful) { - if (ProxyUtils.isCONNECT(initialRequest)) { - finishCONNECT(serverConnection, initialRequest); - } else { - recordServerConnectionResult(serverConnection, initialRequest, - connectionSuccessful); - } - } else { - recordServerConnectionResult(serverConnection, initialRequest, - false); - } + protected void serverConnectionSucceeded( + ProxyToServerConnection serverConnection) { + recordServerConnectionResult(serverConnection, true); + } + + /** + * If the {@link ProxyToServerConnection} fails to complete its connection + * lifecycle successfully, this method is called to let us know about it. + * + * @param serverConnection + * @param lastStateBeforeFailure + */ + protected void serverConnectionFailed( + ProxyToServerConnection serverConnection, + ConnectionState lastStateBeforeFailure) { + recordServerConnectionResult(serverConnection, false); } /** @@ -450,9 +456,9 @@ protected void disableChainingFor(HttpRequest request) { * Connection Management **************************************************************************/ - private ProxyToServerConnection connectToServer(HttpRequest request, + private ProxyToServerConnection connectToServer(HttpRequest httpRequest, + TransportProtocol transportProtocol, SSLContext sslContext, String hostAndPort, String serverHostAndPort, - TransportProtocol transportProtocol, String chainedProxyHostAndPort) throws UnknownHostException { LOG.debug("Establishing new ProxyToServerConnection"); @@ -463,124 +469,29 @@ private ProxyToServerConnection connectToServer(HttpRequest request, serverHostAndPort); } ProxyToServerConnection connection = new ProxyToServerConnection( - this.proxyServer, this, transportProtocol, address, + this.proxyServer, this, transportProtocol, sslContext, address, serverHostAndPort, chainedProxyHostAndPort, responseFilter); serverConnectionsByHostAndPort.put(hostAndPort, connection); return connection; } - private ChannelFuture respondCONNECTSuccessful() { - LOG.debug("Responding with CONNECT successful"); - HttpResponse response = responseFor(HttpVersion.HTTP_1_1, - CONNECTION_ESTABLISHED); - response.headers().set("Connection", "Keep-Alive"); - response.headers().set("Proxy-Connection", "Keep-Alive"); - ProxyUtils.addVia(response); - return writeToChannel(response); - } + protected ConnectionFlowStep respondCONNECTSuccessful = new ConnectionFlowStep( + this, NEGOTIATING_CONNECT, true) { + protected Future execute() { + LOG.debug("Responding with CONNECT successful"); + HttpResponse response = responseFor(HttpVersion.HTTP_1_1, + CONNECTION_ESTABLISHED); + response.headers().set("Connection", "Keep-Alive"); + response.headers().set("Proxy-Connection", "Keep-Alive"); + ProxyUtils.addVia(response); + return writeToChannel(response); + }; + }; /** *

- * Ends the flow for establishing a CONNECT tunnel. The handling is - * different depending on whether we're doing a simple tunnel or acting as - * MITM. - *

- * - *

- * See {@link ProxyToServerConnection#startCONNECT()} for the beginning of - * this flow. - *

- * - * @param serverConnection - * the ProxyToServerConnection that connected - * @param initialRequest - * the HTTPRequest that prompted us to do a CONNECT - */ - private void finishCONNECT(final ProxyToServerConnection serverConnection, - final HttpRequest initialRequest) { - LOG.debug("Handling CONNECT request"); - respondCONNECTSuccessful().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) - throws Exception { - if (proxyServer.isUseMITMInSSL()) { - finishCONNECTWithMITM(serverConnection, initialRequest); - } else { - finishCONNECTWithTunneling(serverConnection, initialRequest); - } - } - }); - } - - /** - *

- * Ends the flow for establishing a simple CONNECT tunnel. - *

- * - *

- * See {@link ProxyToServerConnection#startCONNECTWithTunneling()} for the - * beginning of this flow. - *

- * - * @param serverConnection - * the ProxyToServerConnection that connected - * @param initialRequest - * the HttpRequest that prompted us to do a CONNECT - */ - private void finishCONNECTWithTunneling( - final ProxyToServerConnection serverConnection, - final HttpRequest initialRequest) { - LOG.debug("Finishing tunneling"); - startTunneling().addListener(new GenericFutureListener>() { - @Override - public void operationComplete(Future future) throws Exception { - recordServerConnectionResult(serverConnection, initialRequest, - future.isSuccess()); - if (future.isSuccess()) { - LOG.debug("Tunnel Established"); - } - } - }); - } - - /** - *

- * Ends the flow for establishing a man-in-the-middle tunnel. - *

- * - *

- * See {@link ProxyToServerConnection#startCONNECTWithMITM()} for the - * beginning of this flow. - *

- * - * @param serverConnection - * the ProxyToServerConnection that connected - * @param initialRequest - * the HTTPRequest that prompted us to do a CONNECT - */ - private void finishCONNECTWithMITM( - final ProxyToServerConnection serverConnection, - final HttpRequest initialRequest) { - LOG.debug("Finishing SSL MITM"); - enableSSLAsServer().addListener( - new GenericFutureListener>() { - @Override - public void operationComplete(Future future) - throws Exception { - become(AWAITING_INITIAL); - recordServerConnectionResult(serverConnection, - initialRequest, future.isSuccess()); - if (future.isSuccess()) { - LOG.debug("SSL MITM Established"); - } - } - }); - } - - /** - *

- * Record the result of traying to connect to a server. If we failed to + * Record the result of trying to connect to a server. If we failed to * connect to the server, one of two things can happen: *

* @@ -592,20 +503,21 @@ public void operationComplete(Future future) * * * @param serverConnection - * @param initialRequest * @param connectionSuccessful */ private void recordServerConnectionResult( ProxyToServerConnection serverConnection, - HttpRequest initialRequest, boolean connectionSuccessful) { + boolean connectionSuccessful) { if (this.numberOfCurrentlyConnectingServers.decrementAndGet() == 0) { resumeReading(); + become(AWAITING_INITIAL); } if (connectionSuccessful) { LOG.debug("Connection to server succeeded: {}", serverConnection.getAddress()); numberOfCurrentlyConnectedServers.incrementAndGet(); } else { + HttpRequest initialRequest = serverConnection.getInitialRequest(); if (shouldChain(initialRequest)) { fallbackToDirectConnection(serverConnection, initialRequest); } else { @@ -645,15 +557,6 @@ private void fallbackToDirectConnection( private void initChannelPipeline(ChannelPipeline pipeline) { LOG.debug("Configuring ChannelPipeline"); - if (proxyServer.getSslContextSource() != null) { - LOG.debug("Adding SSL handler"); - SSLEngine engine = proxyServer.getSslContextSource() - .getSSLContext() - .createSSLEngine(); - engine.setUseClientMode(false); - pipeline.addLast("ssl", new SslHandler(engine)); - } - // We want to allow longer request lines, headers, and chunks // respectively. pipeline.addLast("decoder", new ProxyHttpRequestDecoder(8192, 8192 * 2, diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java new file mode 100644 index 000000000..547978de2 --- /dev/null +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java @@ -0,0 +1,97 @@ +package org.littleshoot.proxy.impl; + +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Coordinates the various steps involved in establishing a connection, such as + * establishing a socket connection, SSL handshaking, HTTP CONNECT request + * processing, and so on. + */ +class ConnectionFlow { + private Queue steps = new ConcurrentLinkedQueue(); + + private final ClientToProxyConnection clientConnection; + private final ProxyToServerConnection serverConnection; + private final Object connectLock; + private volatile ConnectionFlowStep currentStep; + private volatile boolean suppressInitialRequest = false; + + ConnectionFlow( + ClientToProxyConnection clientConnection, + ProxyToServerConnection serverConnection, + Object connectLock) { + super(); + this.clientConnection = clientConnection; + this.serverConnection = serverConnection; + this.connectLock = connectLock; + } + + ConnectionFlow startWith(ConnectionFlowStep step) { + return then(step); + } + + ConnectionFlow then(ConnectionFlowStep step) { + steps.add(step); + return this; + } + + void read(Object msg) { + if (this.currentStep != null) { + this.currentStep.read(this, msg); + } + } + + void start() { + clientConnection.serverConnectionFlowStarted(serverConnection); + go(); + } + + void go() { + synchronized (connectLock) { + currentStep = steps.poll(); + if (currentStep == null) { + succeed(); + } else { + final ProxyConnection connection = currentStep.getConnection(); + final ProxyConnectionLogger LOG = connection.getLOG(); + LOG.debug("Executing connection flow step: {}", currentStep); + connection.become(currentStep.getState()); + suppressInitialRequest = suppressInitialRequest + || currentStep.isSuppressInitialRequest(); + currentStep.execute().addListener( + new GenericFutureListener() { + public void operationComplete(Future future) + throws Exception { + if (future.isSuccess()) { + LOG.debug("ConnectionFlowStep succeeded"); + currentStep.onSuccess(ConnectionFlow.this); + } else { + LOG.debug("ConnectionFlowStep failed: {}", + future.cause()); + fail(); + } + }; + }); + } + } + } + + void succeed() { + serverConnection.getLOG().debug( + "Connection flow completed successfully: {}", currentStep); + serverConnection.connectionSucceeded(!suppressInitialRequest); + } + + void fail() { + ConnectionState lastStateBeforeFailure = serverConnection + .getCurrentState(); + serverConnection.disconnect(); + clientConnection.serverConnectionFailed( + serverConnection, + lastStateBeforeFailure); + } +} diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java new file mode 100644 index 000000000..b9e2a6b4c --- /dev/null +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java @@ -0,0 +1,115 @@ +package org.littleshoot.proxy.impl; + +import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.concurrent.Future; + +/** + * Represents a phase in a {@link ConnectionFlow}. + */ +abstract class ConnectionFlowStep { + private final ProxyConnectionLogger LOG; + private final ProxyConnection connection; + private final ConnectionState state; + private final boolean suppressInitialRequest; + + /** + * Construct a new step in a connection flow. This step does not suppress + * the initial {@link HttpRequest}. + * + * @param connection + * the connection that we're working on + * @param state + * the state that the connection will show while we're processing + * this step + */ + ConnectionFlowStep(ProxyConnection connection, + ConnectionState state) { + this(connection, state, false); + } + + /** + * Construct a new step in a connection flow. + * + * @param connection + * the connection that we're working on + * @param state + * the state that the connection will show while we're processing + * this step + * @param suppressInitialRequest + * set to true if the inclusion of this step should prevent the + * initial {@link HttpRequest} that spawned our connection from + * being set after we connect successfully + */ + ConnectionFlowStep(ProxyConnection connection, + ConnectionState state, + boolean suppressInitialRequest) { + super(); + this.connection = connection; + this.state = state; + this.suppressInitialRequest = suppressInitialRequest; + this.LOG = connection.getLOG(); + } + + ProxyConnection getConnection() { + return connection; + } + + ConnectionState getState() { + return state; + } + + boolean isSuppressInitialRequest() { + return suppressInitialRequest; + } + + /** + * When the flow determines that this step was successful, it calls into + * this method. The default implementation simply continues with the flow. + * Other implementations may choose to not continue and instead wait for a + * message or something like that. + * + * @param flow + */ + void onSuccess(ConnectionFlow flow) { + flow.go(); + } + + /** + *

+ * Any messages that are read from the underlying connection while we're at + * this step of the connection flow are passed to this method. + *

+ * + *

+ * The default implementation ignores the message and logs this, since we + * weren't really expecting a message here. + *

+ * + *

+ * Some {@link ConnectionFlowStep}s do need to read the messages, so they + * override this method as appropriate. + *

+ * + * @param flow + * our {@link ConnectionFlow} + * @param msg + * the message read from the underlying connection + */ + void read(ConnectionFlow flow, Object msg) { + LOG.debug("Received message while in the middle of connecting: {}", msg); + } + + /** + * Implement this method to actually do the work involved in this step of + * the flow. + * + * @return + */ + protected abstract Future execute(); + + @Override + public String toString() { + return state.toString(); + } + +} diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java index 3fcea0deb..e25efc9ed 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java @@ -1,19 +1,65 @@ package org.littleshoot.proxy.impl; enum ConnectionState { - CONNECTING, // Connection attempting to connect - HANDSHAKING, // In the middle of doing an SSL handshake - AWAITING_PROXY_AUTHENTICATION, // Connected but waiting for proxy - // authentication, - NEGOTIATING_CONNECT, // In the process of negotiating an HTTP CONNECT from - // the client - AWAITING_CONNECT_OK, // When forwarding a CONNECT to a chained proxy, we - // await the CONNECTION_OK message from the proxy - AWAITING_INITIAL, // Connected and awaiting initial message (e.g. - // HttpRequest or HttpResponse) - AWAITING_CHUNK, // Connected and awaiting HttpContent chunk - TUNNELING, // Connected and tunneling raw ByteBufs - DISCONNECT_REQUESTED, // We've asked the client to disconnect, but it hasn't - // yet - DISCONNECTED // Disconnected + /** + * Connection attempting to connect. + */ + CONNECTING(true), + + /** + * In the middle of doing an SSL handshake. + */ + HANDSHAKING(true), + + /** + * In the process of negotiating an HTTP CONNECT from the client. + */ + NEGOTIATING_CONNECT(true), + + /** + * When forwarding a CONNECT to a chained proxy, we await the CONNECTION_OK + * message from the proxy. + */ + AWAITING_CONNECT_OK(true), + + /** + * Connected but waiting for proxy authentication. + */ + AWAITING_PROXY_AUTHENTICATION, + + /** + * Connected and awaiting initial message (e.g. HttpRequest or + * HttpResponse). + */ + AWAITING_INITIAL, + + /** + * Connected and awaiting HttpContent chunk. + */ + AWAITING_CHUNK, + + /** + * We've asked the client to disconnect, but it hasn't yet. + */ + DISCONNECT_REQUESTED(), + + /** + * Disconnected + */ + DISCONNECTED(); + + private boolean partOfConnectFlow; + + ConnectionState(boolean partOfConnectionFlow) { + this.partOfConnectFlow = partOfConnectionFlow; + } + + ConnectionState() { + this(false); + } + + public boolean isPartOfConnectFlow() { + return partOfConnectFlow; + } + } diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java index 93ea27c90..f7b05d597 100644 --- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java +++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java @@ -277,17 +277,23 @@ public HttpProxyServer addActivityTracker(ActivityTracker activityTracker) { return this; } - public void start() { - start(false, true); + public HttpProxyServer start() { + return start(true, true); } - public void start(final boolean localOnly, final boolean anyAddress) { + public HttpProxyServer start(final boolean localOnly, + final boolean anyAddress) { LOG.info("Starting proxy on port: " + this.port); this.stopped.set(false); ChannelInitializer initializer = new ChannelInitializer() { protected void initChannel(Channel ch) throws Exception { + SSLContext sslContext = null; + if (sslContextSource != null) { + sslContext = sslContextSource.getSSLContext(); + } new ClientToProxyConnection( DefaultHttpProxyServer.this, + sslContext, ch.pipeline()); }; }; @@ -336,6 +342,8 @@ public void run() { stop(); } })); + + return this; } private final AtomicBoolean stopped = new AtomicBoolean(false); @@ -392,19 +400,19 @@ public void stop() { protected void registerChannel(Channel channel) { this.allChannels.add(channel); } - + protected ChainedProxyManager getChainProxyManager() { return chainProxyManager; } - + protected SSLContextSource getSslContextSource() { return sslContextSource; } - + protected ProxyAuthenticator getProxyAuthenticator() { return proxyAuthenticator; } - + protected HttpRequestFilter getRequestFilter() { return requestFilter; } @@ -412,11 +420,11 @@ protected HttpRequestFilter getRequestFilter() { protected HttpResponseFilters getResponseFilters() { return responseFilters; } - + protected Collection getActivityTrackers() { return activityTrackers; } - + protected EventLoopGroup getProxyToServerWorkerFor( TransportProtocol transportProtocol) { return this.proxyToServerWorkerPools.get(transportProtocol); @@ -550,12 +558,23 @@ public DefaultHttpProxyServerBuilder withIdleConnectionTimeout( return this; } - public HttpProxyServer build() { + public DefaultHttpProxyServer build() { return new DefaultHttpProxyServer(transportProtocol, port, sslContextSource, proxyAuthenticator, chainProxyManager, requestFilter, responseFilters, useDnsSec, useMITMInSSL, acceptAllSSLCertificates, transparent, idleConnectionTimeout); } + + /** + * Convenience method that builds and immediately starts the server. + * + * @return + */ + public DefaultHttpProxyServer start() { + DefaultHttpProxyServer server = build(); + server.start(); + return server; + } } } diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java index ebda61b32..4b7a56919 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java @@ -13,11 +13,15 @@ import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleStateEvent; import io.netty.util.ReferenceCounted; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + /** *

* Base class for objects that represent a connection to/from our proxy. @@ -71,11 +75,14 @@ abstract class ProxyConnection extends protected final ProxyConnectionLogger LOG = new ProxyConnectionLogger(this); protected final DefaultHttpProxyServer proxyServer; + protected final SSLContext sslContext; + protected final boolean runsAsSSLClient; protected volatile ChannelHandlerContext ctx; protected volatile Channel channel; private volatile ConnectionState currentState; + private volatile boolean tunneling = false; /** * Construct a new ProxyConnection. @@ -84,11 +91,21 @@ abstract class ProxyConnection extends * the state in which this connection starts out * @param proxyServer * the {@link DefaultHttpProxyServer} in which we're running + * @param sslContext + * (optional) if provided, this connection will be encrypted + * using the given SSLContext + * @param runsAsSSLClient + * determines whether this connection acts as an SSL client or + * server (determines who does the handshake) */ protected ProxyConnection(ConnectionState initialState, - DefaultHttpProxyServer proxyServer) { + DefaultHttpProxyServer proxyServer, + SSLContext sslContext, + boolean runsAsSSLClient) { become(initialState); this.proxyServer = proxyServer; + this.sslContext = sslContext; + this.runsAsSSLClient = runsAsSSLClient; } /*************************************************************************** @@ -103,6 +120,14 @@ protected ProxyConnection(ConnectionState initialState, protected void read(Object msg) { LOG.debug("Reading: {}", msg); + if (tunneling) { + readRaw((ByteBuf) msg); + } else { + readNormal(msg); + } + } + + private void readNormal(Object msg) { ConnectionState nextState = getCurrentState(); switch (getCurrentState()) { case AWAITING_INITIAL: @@ -114,17 +139,14 @@ protected void read(Object msg) { nextState = ProxyUtils.isLastChunk(chunk) ? AWAITING_INITIAL : AWAITING_CHUNK; break; - case TUNNELING: - readRaw((ByteBuf) msg); - break; case AWAITING_PROXY_AUTHENTICATION: if (msg instanceof HttpRequest) { // Once we get an HttpRequest, try to process it as usual nextState = readInitial((I) msg); } else { - // Anything that's not an HttpRequest that came in while we're - // pending authentication gets dropped on the floor. This can - // happen if the connected host already sent us some chunks + // Anything that's not an HttpRequest that came in while + // we're pending authentication gets dropped on the floor. This + // can happen if the connected host already sent us some chunks // (e.g. from a POST) after an initial request that turned out // to require authentication. } @@ -244,8 +266,8 @@ public void operationComplete(ChannelFuture future) * Lifecycle **************************************************************************/ - protected void connected(ChannelHandlerContext ctx) { - become(is(CONNECTING) ? AWAITING_INITIAL : getCurrentState()); + protected void connected() { + become(AWAITING_INITIAL); LOG.debug("Connected"); } @@ -256,73 +278,54 @@ protected void disconnected() { /** *

- * This method enables tunneling on this connection by dropping the HTTP - * related encoders and decoders, as well as idle timers. This method also - * resumes reading on the underlying channel. + * Enables tunneling on this connection by dropping the HTTP related + * encoders and decoders, as well as idle timers. This method also resumes + * reading on the underlying channel. *

* *

* Note - the work is done on the context's executor because * {@link ChannelPipeline#remove(String)} can deadlock if called directly. *

- * - * @return a Future that tells us when tunneling has been enabled */ - protected Future startTunneling() { - return ctx.executor().submit(new Runnable() { - @Override - public void run() { - ChannelPipeline pipeline = ctx.pipeline(); - if (pipeline.get("encoder") != null) { - pipeline.remove("encoder"); - } - if (pipeline.get("decoder") != null) { - pipeline.remove("decoder"); + protected ConnectionFlowStep startTunneling = new ConnectionFlowStep( + this, NEGOTIATING_CONNECT, true) { + protected Future execute() { + return ctx.executor().submit(new Runnable() { + @Override + public void run() { + ChannelPipeline pipeline = ctx.pipeline(); + if (pipeline.get("encoder") != null) { + pipeline.remove("encoder"); + } + if (pipeline.get("decoder") != null) { + pipeline.remove("decoder"); + } + if (pipeline.get("idle") != null) { + pipeline.remove("idle"); + } + tunneling = true; } - if (pipeline.get("idle") != null) { - pipeline.remove("idle"); - } - become(TUNNELING); - } - }); - } - - /** - * Enables SSL on this connection as a client. - * - * @return a future for when the SSL handshake is complete - */ - protected Future enableSSLAsClient() { - LOG.debug("Enabling SSL as Client"); - return enableSSL(true); - } + }); + }; + }; /** - * Enables SSL on this connection as a server. + * Encrypts traffic on this connection. * - * @return a future for when the SSL handshake is complete + * @return */ - protected Future enableSSLAsServer() { - LOG.debug("Enabling SSL as Server"); - Future future = enableSSL(false); - resumeReading(); - return future; + protected Future encrypt() { + return encrypt(ctx.pipeline()); } - private Future enableSSL(boolean isClient) { - LOG.debug("Enabling SSL"); - ChannelPipeline pipeline = ctx.pipeline(); - // TODO: make this work again - // SslContextFactory scf = new SslContextFactory( - // new SelfSignedKeyStoreManager()); - // SSLContext context = isClient ? scf.getClientContext() : scf - // .getServerContext(); - // SSLEngine engine = context.createSSLEngine(); - // engine.setUseClientMode(isClient); - // SslHandler handler = new SslHandler(engine); - // pipeline.addFirst("ssl", handler); - // return handler.handshakeFuture(); - return null; + protected Future encrypt(ChannelPipeline pipeline) { + LOG.debug("Enabling encryption with SSLContext: {}", sslContext); + SSLEngine engine = sslContext.createSSLEngine(); + engine.setUseClientMode(runsAsSSLClient); + SslHandler handler = new SslHandler(engine); + pipeline.addFirst("ssl", handler); + return handler.handshakeFuture(); } /** @@ -384,6 +387,10 @@ protected boolean is(ConnectionState state) { return currentState == state; } + protected boolean isConnecting() { + return currentState.isPartOfConnectFlow(); + } + /** * Udpates the current state to the given value. * @@ -413,6 +420,10 @@ protected void resumeReading() { this.channel.config().setAutoRead(true); } + ProxyConnectionLogger getLOG() { + return LOG; + } + /*************************************************************************** * Adapting the Netty API **************************************************************************/ @@ -440,7 +451,7 @@ public void channelRegistered(ChannelHandlerContext ctx) throws Exception { @Override public final void channelActive(ChannelHandlerContext ctx) throws Exception { try { - connected(ctx); + connected(); } finally { super.channelActive(ctx); } diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index e87effd1c..31b98c8c9 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -4,10 +4,7 @@ import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; @@ -22,7 +19,6 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.GenericFutureListener; import java.net.InetSocketAddress; import java.util.LinkedList; @@ -31,6 +27,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ssl.SSLContext; + import org.littleshoot.proxy.HttpFilter; import org.littleshoot.proxy.TransportProtocol; import org.littleshoot.proxy.UnknownTransportProtocolError; @@ -43,6 +41,7 @@ public class ProxyToServerConnection extends ProxyConnection { private final ClientToProxyConnection clientConnection; private volatile TransportProtocol transportProtocol; + private volatile SSLContext sslContext; private volatile InetSocketAddress address; private final String serverHostAndPort; private volatile String chainedProxyHostAndPort; @@ -55,6 +54,12 @@ public class ProxyToServerConnection extends ProxyConnection { */ private final Object connectLock = new Object(); + /** + * Encapsulates the flow for establishing a connection, which can vary + * depending on how things are configured. + */ + private volatile ConnectionFlow connectionFlow; + /** * This is the initial request received prior to connecting. We keep track * of it so that we can process it after connection finishes. @@ -98,12 +103,13 @@ public class ProxyToServerConnection extends ProxyConnection { ProxyToServerConnection( DefaultHttpProxyServer proxyServer, ClientToProxyConnection clientConnection, - TransportProtocol transportProtocol, + TransportProtocol transportProtocol, SSLContext sslContext, InetSocketAddress address, String serverHostAndPort, String chainedProxyHostAndPort, HttpFilter responseFilter) { - super(DISCONNECTED, proxyServer); + super(DISCONNECTED, proxyServer, sslContext, true); this.clientConnection = clientConnection; this.transportProtocol = transportProtocol; + this.sslContext = sslContext; this.address = address; this.serverHostAndPort = serverHostAndPort; this.chainedProxyHostAndPort = chainedProxyHostAndPort; @@ -120,24 +126,9 @@ protected void read(Object msg) { // Record statistic for ConnectionTracer and then ignore it clientConnection.recordBytesReceivedFromServer(this, (ConnectionTracer) msg); - } else if (is(AWAITING_CONNECT_OK)) { + } else if (isConnecting()) { LOG.debug("Reading: {}", msg); - // Here we're handling the response from a chained proxy to our - // earlier CONNECT request - boolean connectOk = false; - if (msg instanceof HttpResponse) { - HttpResponse httpResponse = (HttpResponse) msg; - int statusCode = httpResponse.getStatus().code(); - if (statusCode >= 200 && statusCode <= 299) { - connectOk = true; - } - } - if (connectOk) { - // The chained proxy is now tunneling, so we start tunneling too - startCONNECTWithTunneling(); - } else { - unableToConnect(); - } + this.connectionFlow.read(msg); } else { super.read(msg); } @@ -229,15 +220,6 @@ protected void writeHttp(HttpObject httpObject) { * Lifecycle **************************************************************************/ - @Override - protected void connected(ChannelHandlerContext ctx) { - if (ProxyUtils.isCONNECT(initialRequest)) { - startCONNECT(initialRequest); - } else { - finishConnecting(true); - } - } - @Override protected void becameSaturated() { super.becameSaturated(); @@ -301,6 +283,10 @@ public String getChainedProxyHostAndPort() { return chainedProxyHostAndPort; } + public HttpRequest getInitialRequest() { + return initialRequest; + } + /*************************************************************************** * Private Implementation **************************************************************************/ @@ -355,48 +341,112 @@ private void respondWith(HttpObject httpObject) { private void connectAndWrite(final HttpRequest initialRequest) { LOG.debug("Starting new connection to: {}", address); - become(CONNECTING); - // Remember our initial request so that we can write it after connecting this.initialRequest = initialRequest; + initializeConnectionFlow(); + connectionFlow.start(); + } - clientConnection.connectingToServer(this); - - Bootstrap cb = new Bootstrap().group(this.proxyServer - .getProxyToServerWorkerFor(transportProtocol)); - - switch (transportProtocol) { - case TCP: - LOG.debug("Connecting to server with TCP"); - cb.channel(NioSocketChannel.class); - break; - case UDT: - LOG.debug("Connecting to server with UDT"); - cb.channel(NioUdtByteConnectorChannel.class); - break; - default: - throw new UnknownTransportProtocolError(transportProtocol); + /** + * This method initializes our {@link ConnectionFlow} based on however this + * connection has been configured. + */ + private void initializeConnectionFlow() { + this.connectionFlow = new ConnectionFlow(clientConnection, this, + connectLock) + .startWith(connectChannel); + + if (sslContext != null) { + this.connectionFlow.then(encryptChannel); } - cb.handler(new ChannelInitializer() { - protected void initChannel(Channel ch) throws Exception { - initChannelPipeline(ch.pipeline(), initialRequest); - }; - }); - cb.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 40 * 1000); - - cb.connect(address).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) - throws Exception { - if (!future.isSuccess()) { - LOG.debug("Could not connect to " + address, future.cause()); - unableToConnect(); - } + if (ProxyUtils.isCONNECT(initialRequest)) { + if (clientConnection.shouldChain(initialRequest)) { + // If we're chaining to another proxy, send over the CONNECT + // request + this.connectionFlow.then(httpCONNECTWithChainedProxy); + // TODO: add back MITM support + // } else if (this.proxyServer.isUseMITMInSSL()) { + // startCONNECTWithMITM(); } - }); + + this.connectionFlow.then(startTunneling) + .then(clientConnection.respondCONNECTSuccessful) + .then(clientConnection.startTunneling); + } } + private ConnectionFlowStep connectChannel = new ConnectionFlowStep(this, + CONNECTING) { + + @Override + protected Future execute() { + Bootstrap cb = new Bootstrap().group(proxyServer + .getProxyToServerWorkerFor(transportProtocol)); + + switch (transportProtocol) { + case TCP: + LOG.debug("Connecting to server with TCP"); + cb.channel(NioSocketChannel.class); + break; + case UDT: + LOG.debug("Connecting to server with UDT"); + cb.channel(NioUdtByteConnectorChannel.class); + break; + default: + throw new UnknownTransportProtocolError(transportProtocol); + } + + cb.handler(new ChannelInitializer() { + protected void initChannel(Channel ch) throws Exception { + initChannelPipeline(ch.pipeline(), initialRequest); + }; + }); + cb.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 40 * 1000); + + return cb.connect(address); + } + }; + + private ConnectionFlowStep encryptChannel = new ConnectionFlowStep(this, + HANDSHAKING) { + protected Future execute() { + return encrypt(); + } + }; + + private ConnectionFlowStep httpCONNECTWithChainedProxy = new ConnectionFlowStep( + this, AWAITING_CONNECT_OK) { + protected Future execute() { + LOG.debug("Handling CONNECT request through Chained Proxy"); + return writeToChannel(initialRequest); + } + + void onSuccess(ConnectionFlow flow) { + // Do nothing, since we want to wait for the CONNECT response to + // come back + } + + void read(ConnectionFlow flow, Object msg) { + // Here we're handling the response from a chained proxy to our + // earlier CONNECT request + boolean connectOk = false; + if (msg instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) msg; + int statusCode = httpResponse.getStatus().code(); + if (statusCode >= 200 && statusCode <= 299) { + connectOk = true; + } + } + if (connectOk) { + flow.go(); + } else { + flow.fail(); + } + } + + }; + protected void retryConnecting(InetSocketAddress newAddress, TransportProtocol transportProtocol, String chainedProxyHostAndPort, @@ -452,110 +502,32 @@ && shouldFilterResponseTo(httpRequest)) { pipeline.addLast("handler", this); } - /** - *

- * Starts the flow for establishing a CONNECT tunnel. The handling is - * different depending on whether we're doing a simple tunnel or acting as - * man-in-the-middle (MITM). - *

- * - *

- * With a simple tunnel, the proxy simply passes bytes directly between - * client and server. With an MITM tunnel, the proxy terminates an SSL - * connection from the client and another to the server. Every HTTP message - * that is sent between the two is independently handled and forwarded, - * allowing the proxy to inspect and/or modify those messages. - *

- * - *

- * Establishing a tunnel is considered part of the overall connection - * establishment flow, and this connection will remain in the - * {@link ConnectionState#CONNECTING} state until the tunnel has been - * established. - *

- * - *

- * See {@link ClientToProxyConnection#finishCONNECT()} for the end of this - * flow. - *

- * - * @param httpRequest - * the HttpRequest that prompted us to start the CONNECT flow - */ - private void startCONNECT(HttpRequest httpRequest) { - LOG.debug("Handling CONNECT request"); - - if (clientConnection.shouldChain(httpRequest)) { - startCONNECTWithChainedProxy(httpRequest); - } else if (this.proxyServer.isUseMITMInSSL()) { - startCONNECTWithMITM(); - } else { - startCONNECTWithTunneling(); - } - } - - /** - *

- * Start the flow for establishing a simple CONNECT tunnel. - *

- * - *

- * See {@link ClientToProxyConnection#finishCONNECTWithTunneling()} for the - * end of this flow. - *

- * - */ - private void startCONNECTWithTunneling() { - LOG.debug("Preparing to tunnel"); - - startTunneling().addListener(new GenericFutureListener>() { - public void operationComplete(Future future) throws Exception { - finishConnecting(false); - }; - }); - } - - /** - * When we get a CONNECT that needs to go to a chained proxy, we go into - * state AWAITING_CONNECTION_OK and forward the CONNECT. Once we get a - * connection OK (200 status), we consider our connection complete and - * switch to tunneling mode. - * - * @param httpRequest - */ - private void startCONNECTWithChainedProxy(HttpRequest httpRequest) { - LOG.debug("Preparing to tunnel via chained proxy, forwarding CONNECT"); - - become(AWAITING_CONNECT_OK); - writeToChannel(httpRequest); - } - - /** - *

- * Start the flow for establishing a man-in-the-middle tunnel. - *

- * - *

- * See {@link ClientToProxyConnection#finishCONNECTWithMITM()} for the end - * of this flow. - *

- * - * @return - */ - private void startCONNECTWithMITM() { - LOG.debug("Preparing to act as Man-in-the-Middle"); - this.isMITM.set(true); - enableSSLAsClient().addListener( - new GenericFutureListener>() { - @Override - public void operationComplete(Future future) - throws Exception { - LOG.debug("Proxy to server SSL handshake done. Success is: " - + future.isSuccess()); - finishConnecting(false); - } - }); - } + // /** + // *

+ // * Start the flow for establishing a man-in-the-middle tunnel. + // *

+ // * + // *

+ // * See {@link ClientToProxyConnection#finishCONNECTWithMITM()} for the end + // * of this flow. + // *

+ // * + // * @return + // */ + // private void startCONNECTWithMITM() { + // LOG.debug("Preparing to act as Man-in-the-Middle"); + // this.isMITM.set(true); + // encrypt().addListener( + // new GenericFutureListener>() { + // @Override + // public void operationComplete(Future future) + // throws Exception { + // LOG.debug("Proxy to server SSL handshake done. Success is: " + // + future.isSuccess()); + // finishConnecting(false); + // } + // }); + // } /** *

@@ -567,11 +539,11 @@ public void operationComplete(Future future) * whether or not we should forward the initial HttpRequest to * the server after the connection has been established. */ - private void finishConnecting(boolean shouldForwardInitialRequest) { - clientConnection.serverConnected(this, initialRequest, true); + void connectionSucceeded(boolean shouldForwardInitialRequest) { + clientConnection.serverConnectionSucceeded(this); synchronized (connectLock) { - super.connected(ctx); + super.connected(); if (shouldForwardInitialRequest) { LOG.debug("Writing initial request"); @@ -587,16 +559,6 @@ private void finishConnecting(boolean shouldForwardInitialRequest) { } } - /** - * Go back to DISCONNECTED status and let the client know that connecting - * failed. - */ - private void unableToConnect() { - become(DISCONNECTED); - clientConnection.serverConnected(ProxyToServerConnection.this, - initialRequest, false); - } - private void filterResponseIfNecessary(HttpResponse httpResponse) { if (shouldFilterResponseTo(this.currentHttpRequest)) { this.responseFilter.filterResponse( diff --git a/src/test/java/org/littleshoot/proxy/ChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/ChainedProxyTest.java index 0e42bfb49..87cb3822c 100644 --- a/src/test/java/org/littleshoot/proxy/ChainedProxyTest.java +++ b/src/test/java/org/littleshoot/proxy/ChainedProxyTest.java @@ -1,9 +1,15 @@ package org.littleshoot.proxy; +import static org.littleshoot.proxy.TransportProtocol.*; +import io.netty.handler.codec.http.HttpRequest; + import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + import org.junit.Assert; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; /** * Tests a proxy chained to a downstream proxy. In addition to the usual @@ -42,11 +48,35 @@ protected void setUp() { REQUESTS_SENT_BY_UPSTREAM.set(0); REQUESTS_RECEIVED_BY_DOWNSTREAM.set(0); TRANSPORTS_USED.clear(); - this.downstreamProxy = TestUtils - .startProxyServer(TransportProtocol.UDT, DOWNSTREAM_PROXY_PORT); + final SSLContextSource sslContextSource = new SelfSignedSSLContextSource( + "chain_proxy_keystore_1.jks"); + this.downstreamProxy = DefaultHttpProxyServer.configure() + .withPort(DOWNSTREAM_PROXY_PORT) + .withTransportProtocol(UDT) + .withSslContextSource(sslContextSource).start(); this.downstreamProxy.addActivityTracker(DOWNSTREAM_TRACKER); - this.proxyServer = TestUtils.startProxyServer(PROXY_SERVER_PORT, - DOWNSTREAM_PROXY_HOST_AND_PORT); + this.proxyServer = DefaultHttpProxyServer.configure() + .withPort(PROXY_SERVER_PORT) + .withChainProxyManager(new ChainedProxyManagerAdapter() { + public String getHostAndPort(HttpRequest httpRequest) { + return DOWNSTREAM_PROXY_HOST_AND_PORT; + } + + @Override + public TransportProtocol getTransportProtocol() { + return TransportProtocol.UDT; + } + + @Override + public boolean requiresTLSEncryption(HttpRequest httpRequest) { + return true; + } + + @Override + public SSLContext getSSLContext() { + return sslContextSource.getSSLContext(); + } + }).start(); this.proxyServer.addActivityTracker(UPSTREAM_TRACKER); } @@ -79,7 +109,7 @@ private void assertThatDownstreamProxyReceivedSentRequests() { REQUESTS_SENT_BY_UPSTREAM.get(), REQUESTS_RECEIVED_BY_DOWNSTREAM.get()); Assert.assertEquals( - "Only 1 transport protocol should have been used to downstream proxy", + "1 and only 1 transport protocol should have been used to downstream proxy", 1, TRANSPORTS_USED.size()); Assert.assertTrue("UDT transport should have been used", TRANSPORTS_USED.contains(TransportProtocol.UDT)); diff --git a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java index c8883b359..e6915bb6f 100644 --- a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java +++ b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackTest.java @@ -1,5 +1,9 @@ package org.littleshoot.proxy; +import io.netty.handler.codec.http.HttpRequest; + +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + /** * Tests a proxy chained to a missing downstream proxy. When the downstream * proxy is unavailable, the upstream proxy should just fall back to a direct @@ -12,7 +16,14 @@ public class ChainedProxyWithFallbackTest extends BaseProxyTest { @Override protected void setUp() { - this.proxyServer = TestUtils.startProxyServer(PROXY_SERVER_PORT, - DOWNSTREAM_PROXY_HOST_AND_PORT); + this.proxyServer = DefaultHttpProxyServer.configure() + .withPort(PROXY_SERVER_PORT) + .withChainProxyManager(new ChainedProxyManagerAdapter() { + @Override + public String getHostAndPort(HttpRequest httpRequest) { + return DOWNSTREAM_PROXY_HOST_AND_PORT; + } + }) + .start(); } } diff --git a/src/test/java/org/littleshoot/proxy/IdleTest.java b/src/test/java/org/littleshoot/proxy/IdleTest.java index f24f25612..a0bcb4789 100644 --- a/src/test/java/org/littleshoot/proxy/IdleTest.java +++ b/src/test/java/org/littleshoot/proxy/IdleTest.java @@ -32,8 +32,9 @@ public class IdleTest { public void setup() throws Exception { webServer = new Server(WEB_SERVER_PORT); webServer.start(); - proxyServer = (DefaultHttpProxyServer) TestUtils - .startProxyServer(PROXY_PORT); + proxyServer = DefaultHttpProxyServer.configure() + .withPort(PROXY_PORT) + .start(); originalIdleTimeout = proxyServer.getIdleConnectionTimeout(); proxyServer.setIdleConnectionTimeout(10); diff --git a/src/test/java/org/littleshoot/proxy/SelfSignedSSLContextSource.java b/src/test/java/org/littleshoot/proxy/SelfSignedSSLContextSource.java index 38e580f1b..b0a77c004 100644 --- a/src/test/java/org/littleshoot/proxy/SelfSignedSSLContextSource.java +++ b/src/test/java/org/littleshoot/proxy/SelfSignedSSLContextSource.java @@ -17,32 +17,37 @@ /** * Basic {@link SSLContextSource} for unit testing. The {@link SSLContext} uses - * self-signed certificates that are generated automatically on startup. + * self-signed certificates that are generated lazily if the given key store + * file doesn't yet exist. */ public class SelfSignedSSLContextSource implements SSLContextSource { private static final Logger LOG = LoggerFactory .getLogger(SelfSignedSSLContextSource.class); - private static final File KEYSTORE_FILE = new File( - "littleproxy_keystore.jks"); 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 SSLContext sslContext; - public SelfSignedSSLContextSource() { + public SelfSignedSSLContextSource(String keyStorePath) { + this.keyStoreFile = new File(keyStorePath); initializeKeyStore(); initializeSSLContext(); } + public SelfSignedSSLContextSource() { + this("littleproxy_keystore.jks"); + } + @Override public SSLContext getSSLContext() { return sslContext; } private void initializeKeyStore() { - if (KEYSTORE_FILE.isFile()) { + if (keyStoreFile.isFile()) { LOG.info("Not deleting keystore"); return; } @@ -50,10 +55,10 @@ private void initializeKeyStore() { nativeCall("keytool", "-genkey", "-alias", ALIAS, "-keysize", "4096", "-validity", "36500", "-keyalg", "RSA", "-dname", "CN=littleproxy", "-keypass", PASSWORD, "-storepass", - PASSWORD, "-keystore", KEYSTORE_FILE.getName()); + PASSWORD, "-keystore", keyStoreFile.getName()); nativeCall("keytool", "-exportcert", "-alias", ALIAS, "-keystore", - KEYSTORE_FILE.getName(), "-storepass", PASSWORD, "-file", + keyStoreFile.getName(), "-storepass", PASSWORD, "-file", "littleproxy_cert"); } @@ -68,7 +73,7 @@ private void initializeSSLContext() { final KeyStore ks = KeyStore.getInstance("JKS"); // ks.load(new FileInputStream("keystore.jks"), // "changeit".toCharArray()); - ks.load(new FileInputStream(KEYSTORE_FILE), PASSWORD.toCharArray()); + ks.load(new FileInputStream(keyStoreFile), PASSWORD.toCharArray()); // Set up key manager factory to use our key store final KeyManagerFactory kmf = diff --git a/src/test/java/org/littleshoot/proxy/SimpleProxyTest.java b/src/test/java/org/littleshoot/proxy/SimpleProxyTest.java index fb056e5ea..9964723f5 100644 --- a/src/test/java/org/littleshoot/proxy/SimpleProxyTest.java +++ b/src/test/java/org/littleshoot/proxy/SimpleProxyTest.java @@ -1,11 +1,15 @@ package org.littleshoot.proxy; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + /** * Tests just a single basic proxy. */ public class SimpleProxyTest extends BaseProxyTest { @Override protected void setUp() { - this.proxyServer = TestUtils.startProxyServer(PROXY_SERVER_PORT); + this.proxyServer = DefaultHttpProxyServer.configure() + .withPort(PROXY_SERVER_PORT) + .start(); } } diff --git a/src/test/java/org/littleshoot/proxy/TestUtils.java b/src/test/java/org/littleshoot/proxy/TestUtils.java index 47b4c2290..753f0565f 100644 --- a/src/test/java/org/littleshoot/proxy/TestUtils.java +++ b/src/test/java/org/littleshoot/proxy/TestUtils.java @@ -1,7 +1,5 @@ package org.littleshoot.proxy; -import io.netty.handler.codec.http.HttpRequest; - import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -30,132 +28,12 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.ssl.SslSocketConnector; -import org.littleshoot.proxy.impl.DefaultHttpProxyServer; -import org.littleshoot.proxy.impl.DefaultHttpProxyServer.DefaultHttpProxyServerBuilder; public class TestUtils { private TestUtils() { } - public static HttpProxyServer startProxyServer( - int port) { - return startProxyServer(TransportProtocol.TCP, port); - } - - /** - * Creates and starts a proxy server that listens on given port. - * - * @param port - * The port - * @return The instance of proxy server - */ - public static HttpProxyServer startProxyServer( - TransportProtocol transportProtocol, int port) { - return startProxyServerWithCredentials(transportProtocol, port, null, - null); - } - - public static HttpProxyServer startProxyServerWithCredentials( - int port, - String username, String password) { - return startProxyServerWithCredentials(TransportProtocol.TCP, port, - username, password); - } - - /** - * Creates and starts a proxy server that listens on given port and - * authenticates clients with the given username and password. - * - * @param transportProtocol - * the data transport protocol to use - * @param port - * The port - * @param username - * @param password - * @return The instance of proxy server - */ - public static HttpProxyServer startProxyServerWithCredentials( - TransportProtocol transportProtocol, int port, - String username, String password) { - return startProxyServerWithCredentials(transportProtocol, port, null, - username, password); - } - - public static HttpProxyServer startProxyServer( - int port, - final String chainProxyHostAndPort) { - return startProxyServer(TransportProtocol.TCP, port, - chainProxyHostAndPort); - } - - /** - * Creates and starts a proxy server that listens on the given port and - * chains requests to the proxy at the given chainProxyHostAndPort. - * - * @param transportProtocol - * the data transport protocol to use - * @param port - * The port - * @param chainProxyHostAndPort - * Proxy relay - * @return The instance of proxy server - */ - public static HttpProxyServer startProxyServer( - TransportProtocol transportProtocol, int port, - final String chainProxyHostAndPort) { - return startProxyServerWithCredentials(transportProtocol, port, - chainProxyHostAndPort, - null, null); - } - - /** - * Creates and starts a proxy server that listens on the given port, chains - * requests to the proxy at the given chainProxyHostAndPort, and - * authenticates clients using the given username and password. - * - * @param transportProtocol - * the data transport protocol to use - * @param port - * The port - * @param chainProxyHostAndPort - * Proxy relay - * @param username - * @param password - * @return The instance of proxy server - */ - public static HttpProxyServer startProxyServerWithCredentials( - TransportProtocol transportProtocol, int port, - final String chainProxyHostAndPort, final String username, - final String password) { - DefaultHttpProxyServerBuilder builder = DefaultHttpProxyServer - .configure() - .withPort(port); - if (chainProxyHostAndPort != null) { - builder.withTransportProtocol(transportProtocol) - .withChainProxyManager(new ChainedProxyManagerAdapter() { - public String getHostAndPort(HttpRequest httpRequest) { - return chainProxyHostAndPort; - } - - @Override - public TransportProtocol getTransportProtocol() { - return TransportProtocol.UDT; - } - }); - } - if (username != null && password != null) { - builder.withProxyAuthenticator(new ProxyAuthenticator() { - public boolean authenticate(String u, String p) { - return username.equals(u) && password.equals(p); - } - }); - } - HttpProxyServer proxyServer = builder.build(); - proxyServer.start(true, true); - return proxyServer; - } - /** * Creates and starts embedded web server that is running on given port. * Each response has a body that indicates how many bytes were received with diff --git a/src/test/java/org/littleshoot/proxy/UsernamePasswordAuthenticatingProxyTest.java b/src/test/java/org/littleshoot/proxy/UsernamePasswordAuthenticatingProxyTest.java index f310a3bd2..86e6d7b4b 100644 --- a/src/test/java/org/littleshoot/proxy/UsernamePasswordAuthenticatingProxyTest.java +++ b/src/test/java/org/littleshoot/proxy/UsernamePasswordAuthenticatingProxyTest.java @@ -1,14 +1,18 @@ package org.littleshoot.proxy; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + /** * Tests a single proxy that requires username/password authentication. */ -public class UsernamePasswordAuthenticatingProxyTest extends BaseProxyTest { +public class UsernamePasswordAuthenticatingProxyTest extends BaseProxyTest + implements ProxyAuthenticator { @Override protected void setUp() { - this.proxyServer = TestUtils.startProxyServerWithCredentials( - PROXY_SERVER_PORT, getUsername(), - getPassword()); + this.proxyServer = DefaultHttpProxyServer.configure() + .withPort(PROXY_SERVER_PORT) + .withProxyAuthenticator(this) + .start(); } @Override @@ -20,4 +24,9 @@ protected String getUsername() { protected String getPassword() { return "user2"; } + + @Override + public boolean authenticate(String userName, String password) { + return getUsername().equals(userName) && getPassword().equals(password); + } } diff --git a/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java b/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java index 78a544df9..5358163e2 100644 --- a/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java +++ b/src/test/java/org/littleshoot/proxy/VariableSpeedClientServerTest.java @@ -1,6 +1,6 @@ package org.littleshoot.proxy; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import java.io.BufferedReader; import java.io.InputStream; @@ -22,6 +22,7 @@ import org.apache.http.util.EntityUtils; import org.junit.Ignore; import org.junit.Test; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; /** * Tests cases where either the client or the server is slower than the other. @@ -40,7 +41,7 @@ public class VariableSpeedClientServerTest { public void testServerFaster() throws Exception { startServer(); Thread.yield(); - TestUtils.startProxyServer(PROXY_PORT); + DefaultHttpProxyServer.configure().withPort(PROXY_PORT).start(); Thread.yield(); Thread.sleep(400); final DefaultHttpClient client = new DefaultHttpClient(); diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties index f2a84a78f..d7f50ec4b 100644 --- a/src/test/resources/log4j.properties +++ b/src/test/resources/log4j.properties @@ -27,8 +27,8 @@ log4j.appender.TextFile.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%-6r %d{ISO8601} %-5p [%t] %c{2} (%F:%L).%M() - %m%n log4j.appender.TextFile.layout.ConversionPattern=%-6r %d{ISO8601} %-5p [%t] %c{2} (%F:%L).%M() - %m%n -#log4j.logger.org.apache.http=DEBUG -#log4j.logger.org.apache.http.wire=ERROR -log4j.logger.org.littleshoot.proxy=INFO +log4j.logger.org.apache.http=DEBUG +log4j.logger.org.apache.http.wire=ERROR +log4j.logger.org.littleshoot.proxy=DEBUG #log4j.logger.org.littleshoot.proxy.HttpRelayingHandler=off log4j.logger.org.eclipse.jetty=off