All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.asynchttpclient.netty.handler.HttpProtocol Maven / Gradle / Ivy

/*
 * Copyright (c) 2014 AsyncHttpClient Project. All rights reserved.
 *
 * This program is licensed to you under the Apache License Version 2.0,
 * and you may not use this file except in compliance with the Apache License Version 2.0.
 * You may obtain a copy of the Apache License Version 2.0 at
 *     http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Apache License Version 2.0 is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
 */
package org.asynchttpclient.netty.handler;

import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static org.asynchttpclient.Dsl.realm;
import static org.asynchttpclient.util.AuthenticatorUtils.getHeaderWithPrefix;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.LastHttpContent;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;

import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.AsyncHandler.State;
import org.asynchttpclient.AsyncHttpClientConfig;
import org.asynchttpclient.Realm;
import org.asynchttpclient.Realm.AuthScheme;
import org.asynchttpclient.Request;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.handler.StreamedAsyncHandler;
import org.asynchttpclient.netty.Callback;
import org.asynchttpclient.netty.NettyResponseBodyPart;
import org.asynchttpclient.netty.NettyResponseFuture;
import org.asynchttpclient.netty.NettyResponseHeaders;
import org.asynchttpclient.netty.NettyResponseStatus;
import org.asynchttpclient.netty.channel.ChannelManager;
import org.asynchttpclient.netty.channel.ChannelState;
import org.asynchttpclient.netty.channel.Channels;
import org.asynchttpclient.netty.request.NettyRequestSender;
import org.asynchttpclient.ntlm.NtlmEngine;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.spnego.SpnegoEngine;
import org.asynchttpclient.spnego.SpnegoEngineException;
import org.asynchttpclient.uri.Uri;

public final class HttpProtocol extends Protocol {

    public HttpProtocol(ChannelManager channelManager, AsyncHttpClientConfig config, NettyRequestSender requestSender) {
        super(channelManager, config, requestSender);
    }

    private void kerberosChallenge(Channel channel,//
            List authHeaders,//
            Request request,//
            HttpHeaders headers,//
            Realm realm,//
            NettyResponseFuture future) throws SpnegoEngineException {

        Uri uri = request.getUri();
        String host = request.getVirtualHost() == null ? uri.getHost() : request.getVirtualHost();
        String challengeHeader = SpnegoEngine.instance().generateToken(host);
        headers.set(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challengeHeader);
    }

    private void kerberosProxyChallenge(Channel channel,//
            List proxyAuth,//
            Request request,//
            ProxyServer proxyServer,//
            Realm proxyRealm,//
            HttpHeaders headers,//
            NettyResponseFuture future) throws SpnegoEngineException {

        String challengeHeader = SpnegoEngine.instance().generateToken(proxyServer.getHost());
        headers.set(HttpHeaders.Names.PROXY_AUTHORIZATION, "Negotiate " + challengeHeader);
    }

    private void ntlmChallenge(String authenticateHeader,//
            Request request,//
            HttpHeaders headers,//
            Realm realm,//
            NettyResponseFuture future) {

        if (authenticateHeader.equals("NTLM")) {
            // server replied bare NTLM => we didn't preemptively sent Type1Msg
            String challengeHeader = NtlmEngine.INSTANCE.generateType1Msg();
            // FIXME we might want to filter current NTLM and add (leave other
            // Authorization headers untouched)
            headers.set(HttpHeaders.Names.AUTHORIZATION, "NTLM " + challengeHeader);
            future.getInAuth().set(false);

        } else {
            String serverChallenge = authenticateHeader.substring("NTLM ".length()).trim();
            String challengeHeader = NtlmEngine.INSTANCE.generateType3Msg(realm.getPrincipal(), realm.getPassword(), realm.getNtlmDomain(), realm.getNtlmHost(), serverChallenge);
            // FIXME we might want to filter current NTLM and add (leave other
            // Authorization headers untouched)
            headers.set(HttpHeaders.Names.AUTHORIZATION, "NTLM " + challengeHeader);
        }
    }

    private void ntlmProxyChallenge(String authenticateHeader,//
            Request request,//
            Realm proxyRealm,//
            HttpHeaders headers,//
            NettyResponseFuture future) {

        if (authenticateHeader.equals("NTLM")) {
            // server replied bare NTLM => we didn't preemptively sent Type1Msg
            String challengeHeader = NtlmEngine.INSTANCE.generateType1Msg();
            // FIXME we might want to filter current NTLM and add (leave other
            // Authorization headers untouched)
            headers.set(HttpHeaders.Names.PROXY_AUTHORIZATION, "NTLM " + challengeHeader);
            future.getInProxyAuth().set(false);

        } else {
            String serverChallenge = authenticateHeader.substring("NTLM ".length()).trim();
            String challengeHeader = NtlmEngine.INSTANCE.generateType3Msg(proxyRealm.getPrincipal(), proxyRealm.getPassword(), proxyRealm.getNtlmDomain(),
                    proxyRealm.getNtlmHost(), serverChallenge);
            // FIXME we might want to filter current NTLM and add (leave other
            // Authorization headers untouched)
            headers.set(HttpHeaders.Names.PROXY_AUTHORIZATION, "NTLM " + challengeHeader);
        }
    }

    private void finishUpdate(final NettyResponseFuture future, Channel channel, boolean expectOtherChunks) throws IOException {

        future.cancelTimeouts();

        boolean keepAlive = future.isKeepAlive();
        if (expectOtherChunks && keepAlive)
            channelManager.drainChannelAndOffer(channel, future);
        else
            channelManager.tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, future.getPartitionKey());

        try {
            future.done();
        } catch (Exception t) {
            // Never propagate exception once we know we are done.
            logger.debug(t.getMessage(), t);
        }
    }

    private boolean updateBodyAndInterrupt(NettyResponseFuture future, AsyncHandler handler, NettyResponseBodyPart bodyPart) throws Exception {
        boolean interrupt = handler.onBodyPartReceived(bodyPart) != State.CONTINUE;
        if (bodyPart.isUnderlyingConnectionToBeClosed())
            future.setKeepAlive(false);
        return interrupt;
    }

    private boolean exitAfterHandling100(final Channel channel, final NettyResponseFuture future, int statusCode) {
        if (statusCode == CONTINUE.code()) {
            future.setHeadersAlreadyWrittenOnContinue(true);
            future.setDontWriteBodyBecauseExpectContinue(false);
            // directly send the body
            Channels.setAttribute(channel, new Callback(future) {
                @Override
                public void call() throws IOException {
                    Channels.setAttribute(channel, future);
                    requestSender.writeRequest(future, channel);
                }
            });
            return true;
        }
        return false;
    }

    private boolean exitAfterHandling401(//
            final Channel channel,//
            final NettyResponseFuture future,//
            HttpResponse response,//
            final Request request,//
            int statusCode,//
            Realm realm,//
            ProxyServer proxyServer) {

        if (statusCode != UNAUTHORIZED.code())
            return false;

        if (realm == null) {
            logger.info("Can't handle 401 as there's no realm");
            return false;
        }

        if (future.getInAuth().getAndSet(true)) {
            logger.info("Can't handle 401 as auth was already performed");
            return false;
        }

        List wwwAuthHeaders = response.headers().getAll(HttpHeaders.Names.WWW_AUTHENTICATE);

        if (wwwAuthHeaders.isEmpty()) {
            logger.info("Can't handle 401 as response doesn't contain WWW-Authenticate headers");
            return false;
        }

        // FIXME what's this???
        future.setChannelState(ChannelState.NEW);
        HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders());

        switch (realm.getScheme()) {
        case BASIC:
            if (getHeaderWithPrefix(wwwAuthHeaders, "Basic") == null) {
                logger.info("Can't handle 401 with Basic realm as WWW-Authenticate headers don't match");
                return false;
            }

            if (realm.isUsePreemptiveAuth()) {
                // FIXME do we need this, as future.getAndSetAuth
                // was tested above?
                // auth was already performed, most likely auth
                // failed
                logger.info("Can't handle 401 with Basic realm as auth was preemptive and already performed");
                return false;
            }

            // FIXME do we want to update the realm, or directly
            // set the header?
            Realm newBasicRealm = realm(realm)//
                    .setUsePreemptiveAuth(true)//
                    .build();
            future.setRealm(newBasicRealm);
            break;

        case DIGEST:
            String digestHeader = getHeaderWithPrefix(wwwAuthHeaders, "Digest");
            if (digestHeader == null) {
                logger.info("Can't handle 401 with Digest realm as WWW-Authenticate headers don't match");
                return false;
            }
            Realm newDigestRealm = realm(realm)//
                    .setUri(request.getUri())//
                    .setMethodName(request.getMethod())//
                    .setUsePreemptiveAuth(true)//
                    .parseWWWAuthenticateHeader(digestHeader)//
                    .build();
            future.setRealm(newDigestRealm);
            break;

        case NTLM:
            String ntlmHeader = getHeaderWithPrefix(wwwAuthHeaders, "NTLM");
            if (ntlmHeader == null) {
                logger.info("Can't handle 401 with NTLM realm as WWW-Authenticate headers don't match");
                return false;
            }

            ntlmChallenge(ntlmHeader, request, requestHeaders, realm, future);
            Realm newNtlmRealm = realm(realm)//
                    .setUsePreemptiveAuth(true)//
                    .build();
            future.setRealm(newNtlmRealm);
            break;

        case KERBEROS:
        case SPNEGO:
            if (getHeaderWithPrefix(wwwAuthHeaders, "Negociate") == null) {
                logger.info("Can't handle 401 with Kerberos or Spnego realm as WWW-Authenticate headers don't match");
                return false;
            }
            try {
                kerberosChallenge(channel, wwwAuthHeaders, request, requestHeaders, realm, future);

            } catch (SpnegoEngineException e) {
                // FIXME
                String ntlmHeader2 = getHeaderWithPrefix(wwwAuthHeaders, "NTLM");
                if (ntlmHeader2 != null) {
                    logger.warn("Kerberos/Spnego auth failed, proceeding with NTLM");
                    ntlmChallenge(ntlmHeader2, request, requestHeaders, realm, future);
                    Realm newNtlmRealm2 = realm(realm)//
                            .setScheme(AuthScheme.NTLM)//
                            .setUsePreemptiveAuth(true)//
                            .build();
                    future.setRealm(newNtlmRealm2);
                } else {
                    requestSender.abort(channel, future, e);
                    return false;
                }
            }
            break;
        default:
            throw new IllegalStateException("Invalid Authentication scheme " + realm.getScheme());
        }

        final Request nextRequest = new RequestBuilder(future.getCurrentRequest()).setHeaders(requestHeaders).build();

        logger.debug("Sending authentication to {}", request.getUri());
        if (future.isKeepAlive() && !HttpHeaders.isTransferEncodingChunked(response)) {
            future.setReuseChannel(true);
            requestSender.drainChannelAndExecuteNextRequest(channel, future, nextRequest);
        } else {
            channelManager.closeChannel(channel);
            requestSender.sendNextRequest(nextRequest, future);
        }

        return true;
    }

    private boolean exitAfterHandling407(//
            Channel channel,//
            NettyResponseFuture future,//
            HttpResponse response,//
            Request request,//
            int statusCode,//
            ProxyServer proxyServer) {

        if (statusCode != PROXY_AUTHENTICATION_REQUIRED.code())
            return false;

        if (future.getInProxyAuth().getAndSet(true)) {
            logger.info("Can't handle 407 as auth was already performed");
            return false;
        }

        Realm proxyRealm = future.getProxyRealm();

        if (proxyRealm == null) {
            logger.info("Can't handle 407 as there's no proxyRealm");
            return false;
        }

        List proxyAuthHeaders = response.headers().getAll(HttpHeaders.Names.PROXY_AUTHENTICATE);

        if (proxyAuthHeaders.isEmpty()) {
            logger.info("Can't handle 407 as response doesn't contain Proxy-Authenticate headers");
            return false;
        }

        // FIXME what's this???
        future.setChannelState(ChannelState.NEW);
        HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders());

        switch (proxyRealm.getScheme()) {
        case BASIC:
            if (getHeaderWithPrefix(proxyAuthHeaders, "Basic") == null) {
                logger.info("Can't handle 407 with Basic realm as Proxy-Authenticate headers don't match");
                return false;
            }

            if (proxyRealm.isUsePreemptiveAuth()) {
                // FIXME do we need this, as future.getAndSetAuth
                // was tested above?
                // auth was already performed, most likely auth
                // failed
                logger.info("Can't handle 407 with Basic realm as auth was preemptive and already performed");
                return false;
            }

            // FIXME do we want to update the realm, or directly
            // set the header?
            Realm newBasicRealm = realm(proxyRealm)//
                    .setUsePreemptiveAuth(true)//
                    .build();
            future.setProxyRealm(newBasicRealm);
            break;

        case DIGEST:
            String digestHeader = getHeaderWithPrefix(proxyAuthHeaders, "Digest");
            if (digestHeader == null) {
                logger.info("Can't handle 407 with Digest realm as Proxy-Authenticate headers don't match");
                return false;
            }
            Realm newDigestRealm = realm(proxyRealm)//
                    .setUri(request.getUri())//
                    .setMethodName(request.getMethod())//
                    .setUsePreemptiveAuth(true)//
                    .parseProxyAuthenticateHeader(digestHeader)//
                    .build();
            future.setProxyRealm(newDigestRealm);
            break;

        case NTLM:
            String ntlmHeader = getHeaderWithPrefix(proxyAuthHeaders, "NTLM");
            if (ntlmHeader == null) {
                logger.info("Can't handle 407 with NTLM realm as Proxy-Authenticate headers don't match");
                return false;
            }
            ntlmProxyChallenge(ntlmHeader, request, proxyRealm, requestHeaders, future);
            Realm newNtlmRealm = realm(proxyRealm)//
                    .setUsePreemptiveAuth(true)//
                    .build();
            future.setProxyRealm(newNtlmRealm);
            break;

        case KERBEROS:
        case SPNEGO:
            if (getHeaderWithPrefix(proxyAuthHeaders, "Negociate") == null) {
                logger.info("Can't handle 407 with Kerberos or Spnego realm as Proxy-Authenticate headers don't match");
                return false;
            }
            try {
                kerberosProxyChallenge(channel, proxyAuthHeaders, request, proxyServer, proxyRealm, requestHeaders, future);

            } catch (SpnegoEngineException e) {
                // FIXME
                String ntlmHeader2 = getHeaderWithPrefix(proxyAuthHeaders, "NTLM");
                if (ntlmHeader2 != null) {
                    logger.warn("Kerberos/Spnego proxy auth failed, proceeding with NTLM");
                    ntlmChallenge(ntlmHeader2, request, requestHeaders, proxyRealm, future);
                    Realm newNtlmRealm2 = realm(proxyRealm)//
                            .setScheme(AuthScheme.NTLM)//
                            .setUsePreemptiveAuth(true)//
                            .build();
                    future.setProxyRealm(newNtlmRealm2);
                } else {
                    requestSender.abort(channel, future, e);
                    return false;
                }
            }
            break;
        default:
            throw new IllegalStateException("Invalid Authentication scheme " + proxyRealm.getScheme());
        }

        RequestBuilder nextRequestBuilder = new RequestBuilder(future.getCurrentRequest()).setHeaders(requestHeaders);
        if (future.getCurrentRequest().getUri().isSecured()) {
            nextRequestBuilder.setMethod(HttpMethod.CONNECT.name());
        }
        final Request nextRequest = nextRequestBuilder.build();

        logger.debug("Sending proxy authentication to {}", request.getUri());
        if (future.isKeepAlive() && !HttpHeaders.isTransferEncodingChunked(response)) {
            future.setConnectAllowed(true);
            future.setReuseChannel(true);
            requestSender.drainChannelAndExecuteNextRequest(channel, future, nextRequest);
        } else {
            channelManager.closeChannel(channel);
            requestSender.sendNextRequest(nextRequest, future);
        }

        return true;
    }

    private boolean exitAfterHandlingConnect(//
            final Channel channel,//
            final NettyResponseFuture future,//
            final Request request,//
            ProxyServer proxyServer,//
            int statusCode,//
            HttpRequest httpRequest) throws IOException {

        if (statusCode == OK.code() && httpRequest.getMethod() == HttpMethod.CONNECT) {

            if (future.isKeepAlive())
                future.attachChannel(channel, true);

            Uri requestUri = request.getUri();
            logger.debug("Connecting to proxy {} for scheme {}", proxyServer, requestUri.getScheme());

            try {
                channelManager.upgradeProtocol(channel.pipeline(), requestUri);
                future.setReuseChannel(true);
                future.setConnectAllowed(false);
                requestSender.drainChannelAndExecuteNextRequest(channel, future, new RequestBuilder(future.getTargetRequest()).build());

            } catch (GeneralSecurityException ex) {
                requestSender.abort(channel, future, ex);
            }

            return true;
        }

        return false;
    }

    private boolean exitAfterHandlingStatus(Channel channel, NettyResponseFuture future, HttpResponse response, AsyncHandler handler, NettyResponseStatus status)
            throws IOException, Exception {
        if (!future.getAndSetStatusReceived(true) && handler.onStatusReceived(status) != State.CONTINUE) {
            finishUpdate(future, channel, HttpHeaders.isTransferEncodingChunked(response));
            return true;
        }
        return false;
    }

    private boolean exitAfterHandlingHeaders(Channel channel, NettyResponseFuture future, HttpResponse response, AsyncHandler handler, NettyResponseHeaders responseHeaders)
            throws IOException, Exception {
        if (!response.headers().isEmpty() && handler.onHeadersReceived(responseHeaders) != State.CONTINUE) {
            finishUpdate(future, channel, HttpHeaders.isTransferEncodingChunked(response));
            return true;
        }
        return false;
    }

    private boolean exitAfterHandlingReactiveStreams(Channel channel, NettyResponseFuture future, HttpResponse response, AsyncHandler handler) throws IOException {
        if (handler instanceof StreamedAsyncHandler) {
            StreamedAsyncHandler streamedAsyncHandler = (StreamedAsyncHandler) handler;
            StreamedResponsePublisher publisher = new StreamedResponsePublisher(channel.eventLoop(), channelManager, future, channel);
            channel.pipeline().addLast(channel.eventLoop(), "streamedAsyncHandler", publisher);
            Channels.setAttribute(channel, publisher);
            if (streamedAsyncHandler.onStream(publisher) != State.CONTINUE) {
                finishUpdate(future, channel, HttpHeaders.isTransferEncodingChunked(response));
                return true;
            }
        }
        return false;
    }

    private boolean handleHttpResponse(final HttpResponse response, final Channel channel, final NettyResponseFuture future, AsyncHandler handler) throws Exception {

        HttpRequest httpRequest = future.getNettyRequest().getHttpRequest();
        ProxyServer proxyServer = future.getProxyServer();
        logger.debug("\n\nRequest {}\n\nResponse {}\n", httpRequest, response);

        // store the original headers so we can re-send all them to
        // the handler in case of trailing headers
        future.setHttpHeaders(response.headers());

        future.setKeepAlive(config.getKeepAliveStrategy().keepAlive(future.getTargetRequest(), httpRequest, response));

        NettyResponseStatus status = new NettyResponseStatus(future.getUri(), config, response, channel);
        int statusCode = response.getStatus().code();
        Request request = future.getCurrentRequest();
        Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm();
        NettyResponseHeaders responseHeaders = new NettyResponseHeaders(response.headers());

        return exitAfterProcessingFilters(channel, future, handler, status, responseHeaders) || //
                exitAfterHandling401(channel, future, response, request, statusCode, realm, proxyServer) || //
                exitAfterHandling407(channel, future, response, request, statusCode, proxyServer) || //
                exitAfterHandling100(channel, future, statusCode) || //
                exitAfterHandlingRedirect(channel, future, response, request, statusCode, realm) || //
                exitAfterHandlingConnect(channel, future, request, proxyServer, statusCode, httpRequest) || //
                exitAfterHandlingStatus(channel, future, response, handler, status) || //
                exitAfterHandlingHeaders(channel, future, response, handler, responseHeaders) || exitAfterHandlingReactiveStreams(channel, future, response, handler);
    }

    private void handleChunk(HttpContent chunk,//
            final Channel channel,//
            final NettyResponseFuture future,//
            AsyncHandler handler) throws IOException, Exception {

        boolean interrupt = false;
        boolean last = chunk instanceof LastHttpContent;

        // Netty 4: the last chunk is not empty
        if (last) {
            LastHttpContent lastChunk = (LastHttpContent) chunk;
            HttpHeaders trailingHeaders = lastChunk.trailingHeaders();
            if (!trailingHeaders.isEmpty()) {
                NettyResponseHeaders responseHeaders = new NettyResponseHeaders(future.getHttpHeaders(), trailingHeaders);
                interrupt = handler.onHeadersReceived(responseHeaders) != State.CONTINUE;
            }
        }

        ByteBuf buf = chunk.content();
        if (!interrupt && !(handler instanceof StreamedAsyncHandler) && (buf.readableBytes() > 0 || last)) {
            NettyResponseBodyPart part = config.getResponseBodyPartFactory().newResponseBodyPart(buf, last);
            interrupt = updateBodyAndInterrupt(future, handler, part);
        }

        if (interrupt || last)
            finishUpdate(future, channel, !last);
    }

    @Override
    public void handle(final Channel channel, final NettyResponseFuture future, final Object e) throws Exception {

        future.touch();

        // future is already done because of an exception or a timeout
        if (future.isDone()) {
            // FIXME isn't the channel already properly closed?
            channelManager.closeChannel(channel);
            return;
        }

        AsyncHandler handler = future.getAsyncHandler();
        try {
            if (e instanceof HttpResponse) {
                if (handleHttpResponse((HttpResponse) e, channel, future, handler))
                    return;

            } else if (e instanceof HttpContent) {
                handleChunk((HttpContent) e, channel, future, handler);
            }
        } catch (Exception t) {
            // e.g. an IOException when trying to open a connection and send the
            // next request
            if (hasIOExceptionFilters//
                    && t instanceof IOException//
                    && requestSender.applyIoExceptionFiltersAndReplayRequest(future, IOException.class.cast(t), channel)) {
                return;
            }

            try {
                requestSender.abort(channel, future, t);
            } catch (Exception abortException) {
                logger.debug("Abort failed", abortException);
            } finally {
                finishUpdate(future, channel, false);
            }
            throw t;
        }
    }

    @Override
    public void onError(NettyResponseFuture future, Throwable error) {
    }

    @Override
    public void onClose(NettyResponseFuture future) {
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy