proxynettyhandlerntlmv2

Netty ProxyHandler writeAndFlush is not writing response to server


Am trying to implement an NTLMProxyHandler in Netty that can perform the NTLM message exchanges and authenticate the client with a web proxy.

The NTLMProxyHandler extends Netty's ProxyHandler class. Due to this an initial HTTP request is triggered by the proxy handler and this reaches the mock proxy server that I have created. The proxy server reads this request and responds with a 407 proxy authentication required response.

The NTLMProxyHandler reads this response on the client side and prepares a new NTLM Type1Message and writes the response to server back again. The problem I am facing is that this request is never sent to my proxy server though the channel future's success handler is called.

I have enabled Netty packages in the logging but unable to figure out why only the response written second time from the ntlm proxy handler is lost.

I have tried using the Netty ProxyHandler's sendToProxyServer(msg) as well as using the channelHandlerCtx passed from channelRead(). In both the cases writeAndFlush is done but the response never reaches the server and the server times out.

Has anyone used the channelHandlerCtx to write back a response to the server and perform a message exchange similar to this ?

  1. Why is the initial request from ntlm proxy handler -> server successful but not successive reponses written from this ntlm proxy handler.
  2. I also see while debugging that even if I shutdown the proxy server while writing the NTLMMessage1, the writeAndFlush future is still successful. Why would the writeAndFlush succeed in this case ?

Any pointers will be really helpful. Thanks !

NTLMProxyHandler.java

    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelPipeline;
    import io.netty.handler.codec.http.DefaultFullHttpRequest;
    import io.netty.handler.codec.http.DefaultFullHttpResponse;
    import io.netty.handler.codec.http.FullHttpResponse;
    import io.netty.handler.codec.http.HttpClientCodec;
    import io.netty.handler.codec.http.HttpContent;
    import io.netty.handler.codec.http.HttpHeaderNames;
    import io.netty.handler.codec.http.HttpHeaders;
    import io.netty.handler.codec.http.HttpMethod;
    import io.netty.handler.codec.http.HttpResponse;
    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.proxy.ProxyConnectException;
    import jcifs.ntlmssp.Type1Message;
    import jcifs.ntlmssp.Type2Message;
    import jcifs.ntlmssp.Type3Message;
    import jcifs.smb.NtlmContext;
    import jcifs.smb.NtlmPasswordAuthentication;
    import jcifs.util.Base64;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import java.net.InetSocketAddress;
    import java.net.SocketAddress;


    public class NTLMProxyHandler extends AbstractProxyHandler {

        private String userName;
        private String password;
        private final static String DOMAIN      = "CORP";
        public static final String NTLM_Prefix = "NTLM";

        private static final Logger logger = LoggerFactory.getLogger(NTLMProxyHandler.class);

        private static int NTLMV2_FLAGS_TYPE3 = 0xa2888205;
        private HttpResponseStatus status;
        private HttpResponse response;

        private NtlmPasswordAuthentication ntlmPasswordAuthentication;
        private NtlmContext ntlmContext;
        private final HttpClientCodec codec = new HttpClientCodec();

        public NTLMProxyHandler(SocketAddress proxyAddress) {
            super(proxyAddress);
        }

        public NTLMProxyHandler(SocketAddress proxyAddress, String domain, String username, String password) {
            super(proxyAddress);
            setConnectTimeoutMillis(50000);
            this.userName = username;
            this.password = password;
            ntlmPasswordAuthentication = new NtlmPasswordAuthentication(DOMAIN, username, password);
            ntlmContext = new NtlmContext(ntlmPasswordAuthentication, true);
        }

        @Override
        public String protocol() {
            return "http";
        }

        @Override
        public String authScheme() {
            return "ntlm";
        }

        protected void addCodec(ChannelHandlerContext ctx) throws Exception {
            ChannelPipeline p = ctx.pipeline();
            String name = ctx.name();
            p.addBefore(name, (String)null, this.codec);
        }

        protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
            this.codec.removeOutboundHandler();
        }

        protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
            this.codec.removeInboundHandler();
        }

        @Override
        protected Object newInitialMessage(ChannelHandlerContext channelHandlerContext) throws Exception {
            InetSocketAddress raddr = this.destinationAddress();
            String rhost;
            if(raddr.isUnresolved()) {
                rhost = raddr.getHostString();
            } else {
                rhost = raddr.getAddress().getHostAddress();
            }

            String host = rhost + ':' + raddr.getPort();
            DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, host, Unpooled.EMPTY_BUFFER, false);
            req.headers().set(HttpHeaderNames.HOST, host);
            req.headers().set("connection", "keep-alive");

// This initial request successfully reaches the server !
            return req;
        }

        @Override
        protected boolean handleResponse(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

            if (o instanceof HttpResponse) {
                 response = (HttpResponse) o;

            }
            boolean finished = o instanceof LastHttpContent;

            if(finished) {
                status = response.status();
                logger.info("Status: " + status);

                if (!response.headers().isEmpty()) {
                    for (String name: response.headers().names()) {
                        for (String value: response.headers().getAll(name)) {
                            logger.debug("Header: " + name + " = " + value);
                        }
                    }
                }
                if(status.code() == 407) {
                    negotiate(channelHandlerContext, response);
                }
                else if(status.code() == 200){
                    logger.info("Client: NTLM exchange complete. Authenticated !");
                }
                else {
                    throw new ProxyConnectException(this.exceptionMessage("status: " + this.status));
                }
            }

            return finished;
        }

        private void negotiate(ChannelHandlerContext channelHandlerContext, HttpResponse msg) throws Exception{
            String ntlmHeader = msg.headers().get(HttpHeaderNames.PROXY_AUTHENTICATE);

            if(ntlmHeader.equalsIgnoreCase("NTLM")){
                logger.info("Client: Creating NTLM Type1Message");
                //Send Type1Message
                byte[] rawType1Message = ntlmContext.initSecContext(new byte[]{}, 0, 0);
                Type1Message type1Message = new Type1Message(rawType1Message);

                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
                String proxyAuthHeader = Base64.encode(type1Message.toByteArray());
                logger.info("Setting proxyAuthHeader = " + proxyAuthHeader);
                response.headers().set(HttpHeaders.Names.PROXY_AUTHORIZATION, proxyAuthHeader);

                ByteBuf byteBuf = Unpooled.buffer(rawType1Message.length);
                byteBuf.writeBytes(response.content());

//This is where the response is lost and never reaches the proxy server
                sendToProxyServer(byteBuf);
                // channelHandlerContext.writeAndFlush(response.content));

            } else if (ntlmHeader.contains(NTLM_Prefix)) {
                logger.info("Client: Creating NTLM Type3Message");
                //Send Type3 Message

            }
        }
    }

Solution

  • I finally figured out the problem. The NTLM proxy handler when it responds to the proxy's message was sending a FullHTTPResponse instead of FullHTTPRequest. Looks like Netty's pipeline was discarding the data written as response and this was not indicated in the logs.

    DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, host, Unpooled.EMPTY_BUFFER, false);
    req.headers().set(HttpHeaderNames.HOST, host);
    req.headers().set(HttpHeaders.Names.PROXY_AUTHORIZATION, "type3message");
    
    sendToProxyServer(req);