* Encapsulates information needed to connect to a chained proxy. @@ -49,6 +51,17 @@ public interface ChainedProxy extends SslEngineSource { */ boolean requiresEncryption(); + /** + * Tell LittleProxy whether or not to do NTLM authentication to the chained + * proxy. Same instance should be returned to maintain state during NLTM + * handshake. + * + * null indicates that we won't do NTLM handshake. + * + * @return NtlmHandler + */ + NtlmHandler getNtlmHandler(); + /** * Filters requests on their way to the chained proxy. * diff --git a/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java b/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java index 61f42881f..2905de919 100644 --- a/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java +++ b/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java @@ -6,6 +6,8 @@ import javax.net.ssl.SSLEngine; +import org.littleshoot.proxy.ntlm.NtlmHandler; + /** * Convenience base class for implementations of {@link ChainedProxy}. */ @@ -40,7 +42,12 @@ public boolean requiresEncryption() { public SSLEngine newSslEngine() { return null; } - + + @Override + public NtlmHandler getNtlmHandler() { + return null; + } + @Override public void filterRequest(HttpObject httpObject) { } diff --git a/src/main/java/org/littleshoot/proxy/FlowContext.java b/src/main/java/org/littleshoot/proxy/FlowContext.java index 15bf9ac89..fd013ee81 100644 --- a/src/main/java/org/littleshoot/proxy/FlowContext.java +++ b/src/main/java/org/littleshoot/proxy/FlowContext.java @@ -7,6 +7,8 @@ import org.littleshoot.proxy.impl.ClientToProxyConnection; +import io.netty.channel.ChannelHandlerContext; + /** *
* Encapsulates contextual information for flow information that's being @@ -16,15 +18,26 @@ public class FlowContext { private final InetSocketAddress clientAddress; private final SSLSession clientSslSession; + private final ChannelHandlerContext ctx; public FlowContext(ClientToProxyConnection clientConnection) { super(); + this.ctx = clientConnection.getContext(); this.clientAddress = clientConnection.getClientAddress(); SSLEngine sslEngine = clientConnection.getSslEngine(); this.clientSslSession = sslEngine != null ? sslEngine.getSession() : null; } + /** + * The client to proxy channel context. + * + * @return + */ + public ChannelHandlerContext getClientToProxyContext() { + return ctx; + } + /** * The address of the client. * diff --git a/src/main/java/org/littleshoot/proxy/FullFlowContext.java b/src/main/java/org/littleshoot/proxy/FullFlowContext.java index 43a4d9fa8..b5251318c 100644 --- a/src/main/java/org/littleshoot/proxy/FullFlowContext.java +++ b/src/main/java/org/littleshoot/proxy/FullFlowContext.java @@ -3,6 +3,8 @@ import org.littleshoot.proxy.impl.ClientToProxyConnection; import org.littleshoot.proxy.impl.ProxyToServerConnection; +import io.netty.channel.ChannelHandlerContext; + /** * Extension of {@link FlowContext} that provides additional information (which * we know after actually processing the request from the client). @@ -10,14 +12,25 @@ public class FullFlowContext extends FlowContext { private final String serverHostAndPort; private final ChainedProxy chainedProxy; + private final ChannelHandlerContext ctx; public FullFlowContext(ClientToProxyConnection clientConnection, ProxyToServerConnection serverConnection) { super(clientConnection); + this.ctx = serverConnection.getContext(); this.serverHostAndPort = serverConnection.getServerHostAndPort(); this.chainedProxy = serverConnection.getChainedProxy(); } + /** + * The proxy to server channel context. + * + * @return + */ + public ChannelHandlerContext getProxyToServerContext() { + return ctx; + } + /** * The host and port for the server (i.e. the ultimate endpoint). * diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java index f7ff1ebd9..09e10f1c1 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java @@ -860,7 +860,7 @@ protected void exceptionCaught(Throwable cause) { private void initChannelPipeline(ChannelPipeline pipeline) { LOG.debug("Configuring ChannelPipeline"); - if (proxyServer.getRequestTracer() != null) { + if (!proxyServer.getActivityTrackers().isEmpty() && proxyServer.getRequestTracer() != null) { pipeline.addLast("requestTracerHandler", new RequestTracerHandler(this)); } @@ -870,8 +870,14 @@ private void initChannelPipeline(ChannelPipeline pipeline) { EventExecutorGroup globalStateWrapperEvenLoop = new GlobalStateWrapperEvenLoop(this); - pipeline.addLast(globalStateWrapperEvenLoop, "bytesReadMonitor", bytesReadMonitor); - pipeline.addLast(globalStateWrapperEvenLoop, "bytesWrittenMonitor", bytesWrittenMonitor); + if(!proxyServer.getActivityTrackers().isEmpty()){ + LOG.info("Activity Trackers are available: {}. Enabled monitoring.", proxyServer.getActivityTrackers().size()); + for (final ActivityTracker activityTracker : proxyServer.getActivityTrackers()) { + LOG.debug("Activity Tracker: {}", activityTracker.getClass()); + } + pipeline.addLast(globalStateWrapperEvenLoop, "bytesReadMonitor", bytesReadMonitor); + pipeline.addLast(globalStateWrapperEvenLoop, "bytesWrittenMonitor", bytesWrittenMonitor); + } pipeline.addLast("proxyProtocolReader", new HttpProxyProtocolRequestDecoder()); @@ -890,8 +896,10 @@ private void initChannelPipeline(ChannelPipeline pipeline) { aggregateContentForFiltering(pipeline, numberOfBytesToBuffer); } - pipeline.addLast(globalStateWrapperEvenLoop, "requestReadMonitor", requestReadMonitor); - pipeline.addLast(globalStateWrapperEvenLoop, "responseWrittenMonitor", responseWrittenMonitor); + if(!proxyServer.getActivityTrackers().isEmpty()){ + pipeline.addLast(globalStateWrapperEvenLoop, "requestReadMonitor", requestReadMonitor); + pipeline.addLast(globalStateWrapperEvenLoop, "responseWrittenMonitor", responseWrittenMonitor); + } pipeline.addLast( "idle", @@ -1349,7 +1357,8 @@ private boolean respondWithShortCircuitResponse(HttpResponse httpResponse) { // if the response is not a Bad Gateway or Gateway Timeout, modify the headers "as if" the short-circuit response were proxied int statusCode = httpResponse.getStatus().code(); - if (statusCode != HttpResponseStatus.BAD_GATEWAY.code() && statusCode != HttpResponseStatus.GATEWAY_TIMEOUT.code()) { + if (statusCode != HttpResponseStatus.BAD_GATEWAY.code() && statusCode != HttpResponseStatus.GATEWAY_TIMEOUT.code() + && statusCode != HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.code()) { modifyResponseHeadersToReflectProxying(httpResponse); } diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java index 5a044bb58..5f4533ac8 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java @@ -54,6 +54,18 @@ ConnectionFlow then(ConnectionFlowStep step) { return this; } + /** + * Indicates whether the given message is relevant to the current step. + * Invoker should check this before calling read(). + * + * @param msg + * the message read from the underlying connection + * @return + */ + boolean isRelevant(Object msg) { + return currentStep.isRelevant(msg); + } + /** * While we're in the process of connecting, any messages read by the * {@link ProxyToServerConnection} are passed to this method, which passes diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java index c60910d59..997df9317 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java @@ -83,6 +83,17 @@ void onSuccess(ConnectionFlow flow) { flow.advance(); } + /** + * Indicates whether the given message is relevant to this step. + * + * @param msg + * the message read from the underlying connection + * @return + */ + boolean isRelevant(Object msg) { + return true; + } + /** *
* Any messages that are read from the underlying connection while we're at diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java index 371fcd586..92c5be784 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java @@ -11,6 +11,12 @@ enum ConnectionState { */ HANDSHAKING(true), + /** + * In the process of sending NTLM negotiate (Type-1) + * and waiting to receive NTLM challenge (Type-2). + */ + NTLM_HANDSHAKING(true), + /** * In the process of negotiating an HTTP CONNECT from the client. */ diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java index eed893c22..cf69153c7 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java @@ -887,21 +887,20 @@ public void write(ChannelHandlerContext ctx, } /** - * Invoked immediately before an HttpRequest is written. - */ + * Invoked immediately before an HttpRequest is written. + */ protected abstract void requestWriting(HttpRequest httpRequest); /** - * Invoked immediately after an HttpRequest has been sent. - */ + * Invoked immediately after an HttpRequest has been sent. + */ protected abstract void requestWritten(HttpRequest httpRequest); /** - * Invoked immediately after an HttpContent has been sent. - */ + * Invoked immediately after an HttpContent has been sent. + */ protected abstract void contentWritten(HttpContent httpContent); } - /** * Utility handler for monitoring responses written on this connection. */ @@ -926,4 +925,7 @@ public void write(ChannelHandlerContext ctx, protected abstract void responseWritten(HttpResponse httpResponse); } + public ChannelHandlerContext getContext() { + return ctx; + } } diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index 3cb87c38c..f5ca864f7 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -1,5 +1,9 @@ package org.littleshoot.proxy.impl; +import static io.netty.handler.codec.http.HttpHeaders.Names.PROXY_AUTHENTICATE; +import static io.netty.handler.codec.http.HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED; +import static org.littleshoot.proxy.impl.ConnectionState.*; + import com.google.common.net.HostAndPort; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ChannelFactory; @@ -15,20 +19,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.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMessage; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpRequestEncoder; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseDecoder; -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.codec.http.LastHttpContent; +import io.netty.handler.codec.http.*; import io.netty.handler.timeout.IdleStateHandler; import io.netty.handler.traffic.GlobalTrafficShapingHandler; import io.netty.util.AttributeKey; @@ -47,6 +38,7 @@ import org.littleshoot.proxy.ExceptionHandler; import org.littleshoot.proxy.TransportProtocol; import org.littleshoot.proxy.UnknownTransportProtocolException; +import org.littleshoot.proxy.ntlm.NtlmException; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLProtocolException; @@ -54,17 +46,11 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.RejectedExecutionException; -import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_CHUNK; -import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_CONNECT_OK; -import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_INITIAL; -import static org.littleshoot.proxy.impl.ConnectionState.CONNECTING; -import static org.littleshoot.proxy.impl.ConnectionState.DISCONNECTED; -import static org.littleshoot.proxy.impl.ConnectionState.HANDSHAKING; - /** *
* Represents a connection from our proxy to a server on the web.
@@ -214,6 +200,13 @@ private ProxyToServerConnection(
@Override
protected void read(Object msg) {
+ enableNtlmIfRequired(msg);
+
+ if (isConnecting() && !connectionFlow.isRelevant(msg)) {
+ LOG.debug("Aborting the current step in the connection flow.");
+ connectionFlow.advance();
+ }
+
if (isConnecting()) {
LOG.debug(
"In the middle of connecting, forwarding message to connection flow: {}",
@@ -224,6 +217,29 @@ protected void read(Object msg) {
}
}
+ private void enableNtlmIfRequired(Object msg) {
+ if (msg instanceof HttpResponse && isNtlmRequired((HttpResponse) msg)) {
+ LOG.debug("NTLM authentication required. Failing the current connection.");
+ connectionFlow.fail(new NtlmException());
+ }
+ }
+
+ private static boolean isNtlmRequired(HttpResponse httpResponse) {
+ if (httpResponse.getStatus().equals(PROXY_AUTHENTICATION_REQUIRED)) {
+ List
* Encrypts the client channel based on our server {@link SSLSession}.
@@ -838,9 +935,7 @@ protected boolean connectionFailed(Throwable cause)
} else {
LOG.info("Connection to upstream server failed", cause);
}
-
- // attempt to connect using a chained proxy, if available
- chainedProxy = availableChainedProxies.poll();
+ chainedProxy = getProxyToConnect(cause);
if (chainedProxy != null) {
LOG.info("Retrying connecting using the next available chained proxy");
@@ -871,6 +966,15 @@ private void resetConnectionForRetry() throws UnknownHostException {
this.setupConnectionParameters();
}
+ private ChainedProxy getProxyToConnect(Throwable cause) {
+ // Retry connection to the same proxy
+ if (cause instanceof NtlmException && chainedProxy.getNtlmHandler() != null) {
+ LOG.debug("Retrying connection to {}", chainedProxy.getChainedProxyAddress());
+ return chainedProxy;
+ }
+ return availableChainedProxies.poll();
+ }
+
/**
* Set up our connection parameters based on server address and chained
* proxies.
@@ -929,7 +1033,7 @@ private void setupConnectionParameters() throws UnknownHostException {
* {@link HttpObjectAggregator} in the {@link ChannelPipeline}.
*
* @param pipeline
- * @param httpRequest
+ * @param channel
*/
private void initChannelPipeline(ChannelPipeline pipeline, Channel channel) {
@@ -943,8 +1047,14 @@ private void initChannelPipeline(ChannelPipeline pipeline, Channel channel) {
final EventExecutorGroup globalStateWrapperEvenLoop = new GlobalStateWrapperEvenLoop(clientConnection, channel.eventLoop());
- pipeline.addLast(globalStateWrapperEvenLoop, "bytesReadMonitor", bytesReadMonitor);
- pipeline.addLast(globalStateWrapperEvenLoop, "bytesWrittenMonitor", bytesWrittenMonitor);
+ if(!proxyServer.getActivityTrackers().isEmpty()) {
+ LOG.info("Activity Trackers are available: {}. Enabled monitoring.", proxyServer.getActivityTrackers().size());
+ for (final ActivityTracker activityTracker : proxyServer.getActivityTrackers()) {
+ LOG.debug("Activity Tracker: {}", activityTracker.getClass());
+ }
+ pipeline.addLast(globalStateWrapperEvenLoop, "bytesReadMonitor", bytesReadMonitor);
+ pipeline.addLast(globalStateWrapperEvenLoop, "bytesWrittenMonitor", bytesWrittenMonitor);
+ }
pipeline.addLast("encoder", new HttpRequestEncoder());
pipeline.addLast("decoder", new HeadAwareHttpResponseDecoder(
@@ -958,9 +1068,10 @@ private void initChannelPipeline(ChannelPipeline pipeline, Channel channel) {
if (numberOfBytesToBuffer > 0) {
aggregateContentForFiltering(pipeline, numberOfBytesToBuffer);
}
-
- pipeline.addLast(globalStateWrapperEvenLoop, "responseReadMonitor", responseReadMonitor);
- pipeline.addLast(globalStateWrapperEvenLoop, "requestWrittenMonitor", requestWrittenMonitor);
+ if(!proxyServer.getActivityTrackers().isEmpty()) {
+ pipeline.addLast(globalStateWrapperEvenLoop, "requestWrittenMonitor", requestWrittenMonitor);
+ pipeline.addLast(globalStateWrapperEvenLoop, "responseReadMonitor", responseReadMonitor);
+ }
// Set idle timeout
pipeline.addLast(
@@ -1001,14 +1112,35 @@ void connectionSucceeded(boolean shouldForwardInitialRequest) {
shouldForwardInitialRequest);
if (shouldForwardInitialRequest) {
- LOG.debug("Writing initial request: {}", initialRequest);
- write(initialRequest);
+ boolean shouldWrite = ntlmAuthenticate(initialRequest);
+ if(shouldWrite) {
+ LOG.debug("Writing initial request: {}", initialRequest);
+ write(initialRequest);
+ }
} else {
LOG.debug("Dropping initial request: {}", initialRequest);
}
}
+ /**
+ * Set authentication details to NTLM proxy if configured.
+ *
+ * @param httpRequest
+ * The request that may need authentication
+ * @return true if the request needs to be written
+ */
+ private boolean ntlmAuthenticate(HttpRequest httpRequest) {
+ if (chainedProxy != null && chainedProxy.getNtlmHandler() != null) {
+ boolean challenged = chainedProxy.getNtlmHandler().isChallenged();
+ if (challenged) {
+ chainedProxy.getNtlmHandler().authenticate(httpRequest);
+ }
+ return challenged;
+ }
+ return true;
+ }
+
/**
* Build an {@link InetSocketAddress} for the given hostAndPort.
*
@@ -1104,5 +1236,4 @@ protected void contentWritten(HttpContent httpContent) {
}
}
};
-
}
diff --git a/src/main/java/org/littleshoot/proxy/ntlm/JcifsNtlmProvider.java b/src/main/java/org/littleshoot/proxy/ntlm/JcifsNtlmProvider.java
new file mode 100644
index 000000000..0c18b06a7
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ntlm/JcifsNtlmProvider.java
@@ -0,0 +1,73 @@
+package org.littleshoot.proxy.ntlm;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jcifs.ntlmssp.NtlmMessage;
+import jcifs.ntlmssp.Type1Message;
+import jcifs.ntlmssp.Type2Message;
+import jcifs.ntlmssp.Type3Message;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static jcifs.ntlmssp.Type3Message.getDefaultFlags;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+
+/**
+ * Reference implementation of {@link NtlmProvider}
+ */
+public class JcifsNtlmProvider implements NtlmProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JcifsNtlmProvider.class);
+
+ private final int flags;
+
+ private final String user;
+
+ private final String password;
+
+ private final String domain;
+
+ private final String workstation;
+
+ private Type2Message type2;
+
+ public JcifsNtlmProvider(int flags, String user, String password, String domain, String workstation) {
+ this.flags = flags > 0 ? flags : getDefaultFlags();
+ this.user = checkNotNull(user);
+ this.password = checkNotNull(password);
+ this.domain = checkNotNull(domain);
+ this.workstation = checkNotNull(workstation);
+ }
+
+ public JcifsNtlmProvider(String user, String password, String domain) {
+ this(0, user, password, domain, EMPTY);
+ }
+
+ @Override
+ public byte[] getType1() {
+ NtlmMessage type1 = new Type1Message(flags, domain, workstation);
+ LOG.debug("NTLM {}", type1);
+ return type1.toByteArray();
+ }
+
+ @Override
+ public boolean setType2(byte[] material) {
+ try {
+ type2 = new Type2Message(material);
+ LOG.debug("NTLM {}", type2);
+ return true;
+ } catch (IOException e) {
+ LOG.warn("Unable to parse NTLM Type2 message", e);
+ return false;
+ }
+ }
+
+ @Override
+ public byte[] getType3() {
+ NtlmMessage type3 = new Type3Message(type2, password, domain, user, workstation, type2.getFlags());
+ LOG.debug("NTLM {}", type3);
+ return type3.toByteArray();
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/ntlm/NtlmException.java b/src/main/java/org/littleshoot/proxy/ntlm/NtlmException.java
new file mode 100644
index 000000000..c8cd7fe7f
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ntlm/NtlmException.java
@@ -0,0 +1,6 @@
+package org.littleshoot.proxy.ntlm;
+
+@SuppressWarnings("serial")
+public class NtlmException extends Exception {
+
+}
diff --git a/src/main/java/org/littleshoot/proxy/ntlm/NtlmHandler.java b/src/main/java/org/littleshoot/proxy/ntlm/NtlmHandler.java
new file mode 100644
index 000000000..66523a1b5
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ntlm/NtlmHandler.java
@@ -0,0 +1,23 @@
+package org.littleshoot.proxy.ntlm;
+
+import org.littleshoot.proxy.ChainedProxy;
+
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+
+/**
+ * This serves as an extension to {@link ChainedProxy} which requires
+ * NTLM authentication. Implementation should set negotiate (Type-11)
+ * and challenge (Type-3) Proxy-Authorization headers to the request.
+ */
+public interface NtlmHandler {
+
+ void negotiate(HttpRequest httpRequest);
+
+ void challenge(HttpResponse httpResponse);
+
+ void authenticate(HttpRequest httpRequest);
+
+ boolean isChallenged();
+
+}
diff --git a/src/main/java/org/littleshoot/proxy/ntlm/NtlmHandlerImpl.java b/src/main/java/org/littleshoot/proxy/ntlm/NtlmHandlerImpl.java
new file mode 100644
index 000000000..6ea4b4452
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ntlm/NtlmHandlerImpl.java
@@ -0,0 +1,85 @@
+package org.littleshoot.proxy.ntlm;
+
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.io.BaseEncoding.base64;
+import static io.netty.handler.codec.http.HttpHeaders.Names.PROXY_AUTHENTICATE;
+import static io.netty.handler.codec.http.HttpHeaders.Names.PROXY_AUTHORIZATION;
+import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
+import static org.apache.commons.lang3.StringUtils.substringAfter;
+
+/**
+ * This class is responsible for writing and reading NTLM related request and
+ * response headers respectively. It delegates the creating of NTLM messages to
+ * a provider.
+ */
+public class NtlmHandlerImpl implements NtlmHandler {
+
+ private final NtlmProvider provider;
+
+ private boolean challenged;
+
+ public NtlmHandlerImpl(NtlmProvider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public void negotiate(HttpRequest httpRequest) {
+ assertChallengeNotRead();
+ writeNegotiation(httpRequest);
+ }
+
+ @Override
+ public void challenge(HttpResponse httpResponse) {
+ assertPersistentConnection(httpResponse);
+ assertChallengeNotRead();
+ readChallenge(httpResponse);
+ assertChallengeRead();
+ }
+
+ @Override
+ public void authenticate(HttpRequest httpRequest) {
+ assertChallengeRead();
+ writeAuthentication(httpRequest);
+ }
+
+ @Override
+ public boolean isChallenged() {
+ return challenged;
+ }
+
+ private void writeNegotiation(HttpRequest httpRequest) {
+ byte[] type1 = provider.getType1();
+ setAuthHeader(httpRequest, type1);
+ }
+
+ private void readChallenge(HttpResponse httpResponse) {
+ String proxyAuth = httpResponse.headers().get(PROXY_AUTHENTICATE);
+ String authChallenge = substringAfter(proxyAuth, "NTLM ");
+ challenged = provider.setType2(base64().decode(authChallenge));
+ }
+
+ private void writeAuthentication(HttpRequest httpRequest) {
+ byte[] type3 = provider.getType3();
+ setAuthHeader(httpRequest, type3);
+ }
+
+ private static void assertPersistentConnection(HttpResponse httpResponse) {
+ checkState(isKeepAlive(httpResponse), "Connection closed during NTLM handshake");
+ }
+
+ private void assertChallengeNotRead() {
+ checkState(!challenged, "NTLM challenge already read");
+ }
+
+ private void assertChallengeRead() {
+ checkState(challenged, "Failed to read NTLM challenge");
+ }
+
+ private static void setAuthHeader(HttpRequest httpRequest, byte[] msg) {
+ httpRequest.headers().set(PROXY_AUTHORIZATION, "NTLM " + base64().encode(msg));
+ }
+
+}
diff --git a/src/main/java/org/littleshoot/proxy/ntlm/NtlmProvider.java b/src/main/java/org/littleshoot/proxy/ntlm/NtlmProvider.java
new file mode 100644
index 000000000..4fb01850a
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ntlm/NtlmProvider.java
@@ -0,0 +1,39 @@
+package org.littleshoot.proxy.ntlm;
+
+/**
+ * Implementation may use some library like jCIFS to deal with various NTLM
+ * messages.
+ *
+ * Reference
+ */
+public interface NtlmProvider {
+
+ /**
+ * This primarily contains a list of features supported by the client and
+ * requested of the server.
+ *
+ * @return Type 1 (negotiation)
+ */
+ byte[] getType1();
+
+ /**
+ * This contains a list of features supported and agreed upon by the server.
+ * Most importantly, however, it contains a challenge generated by the
+ * server.
+ *
+ * @param material
+ * Type 2 (challenge)
+ * @return Whether the client was able to parse the challenge
+ */
+ boolean setType2(byte[] material);
+
+ /**
+ * This contains several pieces of information about the client, including
+ * the domain and username of the client user. It also contains one or more
+ * responses to the Type 2 challenge.
+ *
+ * @return Type 3 (authentication)
+ */
+ byte[] getType3();
+
+}
diff --git a/src/test/java/org/littleshoot/proxy/HttpFilterTest.java b/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
index ca786aa3c..59cf768b8 100644
--- a/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
+++ b/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
@@ -20,6 +20,8 @@
import org.mockserver.matchers.Times;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
@@ -94,6 +96,7 @@ private void setUpHttpProxyServer(HttpFiltersSource filtersSource) {
this.proxyServer = DefaultHttpProxyServer.bootstrap()
.withPort(0)
.withFiltersSource(filtersSource)
+ .plusActivityTracker(getDummyActivityTracker())
.start();
final InetSocketAddress isa = new InetSocketAddress("127.0.0.1", proxyServer.getListenAddress().getPort());
@@ -695,6 +698,7 @@ public HttpFilters filterRequest(HttpRequest originalRequest) {
.withPort(0)
.withFiltersSource(filtersSource)
.withIdleConnectionTimeout(3)
+ .plusActivityTracker(getDummyActivityTracker())
.start();
org.apache.http.HttpResponse httpResponse = HttpClientUtil.performHttpGet("http://localhost:" + mockServerPort + "/servertimeout", proxyServer);
@@ -725,7 +729,7 @@ public HttpFilters filterRequest(HttpRequest originalRequest) {
assertFalse("Expected filter method to not be called", filter.isProxyToServerConnectionFailedInvoked());
assertFalse("Expected filter method to not be called", filter.isProxyToServerConnectionSSLHandshakeStartedInvoked());
}
-
+
@Test
public void testRequestSentInvokedAfterLastHttpContentSent() throws Exception {
final AtomicBoolean lastHttpContentProcessed = new AtomicBoolean(false);
@@ -992,4 +996,63 @@ public void proxyToServerConnectionSSLHandshakeStarted() {
proxyToServerConnectionSSLHandshakeStarted.set(true);
}
}
+
+ private ActivityTracker getDummyActivityTracker() {
+ return new ActivityTracker() {
+ @Override
+ public void clientConnected(final InetSocketAddress clientAddress) {
+
+ }
+
+ @Override
+ public void clientSSLHandshakeSucceeded(final InetSocketAddress clientAddress, final SSLSession sslSession) {
+
+ }
+
+ @Override
+ public void clientDisconnected(final InetSocketAddress clientAddress, final SSLSession sslSession) {
+
+ }
+
+ @Override
+ public void bytesReceivedFromClient(final FlowContext flowContext, final int numberOfBytes) {
+
+ }
+
+ @Override
+ public void requestReceivedFromClient(final FlowContext flowContext, final HttpRequest httpRequest) {
+
+ }
+
+ @Override
+ public void bytesSentToServer(final FullFlowContext flowContext, final int numberOfBytes) {
+
+ }
+
+ @Override
+ public void requestSentToServer(final FullFlowContext flowContext, final HttpRequest httpRequest) {
+
+ }
+
+ @Override
+ public void bytesReceivedFromServer(final FullFlowContext flowContext, final int numberOfBytes) {
+
+ }
+
+ @Override
+ public void responseReceivedFromServer(final FullFlowContext flowContext, final HttpResponse httpResponse) {
+
+ }
+
+ @Override
+ public void bytesSentToClient(final FlowContext flowContext, final int numberOfBytes) {
+
+ }
+
+ @Override
+ public void responseSentToClient(final FlowContext flowContext, final HttpResponse httpResponse) {
+
+ }
+ };
+ }
}