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

kr.jclab.wsman.abstractwsman.netty.client.AbstractNettyHttpConduit Maven / Gradle / Ivy

The newest version!
package kr.jclab.wsman.abstractwsman.netty.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;

import org.apache.cxf.Bus;
import org.apache.cxf.buslifecycle.BusLifeCycleListener;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.helpers.HttpHeaderHelper;
import org.apache.cxf.io.CacheAndWriteOutputStream;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.transport.http.Address;
import org.apache.cxf.transport.http.Headers;
import org.apache.cxf.transport.http.HttpClientHTTPConduit;
import org.apache.cxf.transport.http.netty.client.CxfResponseCallBack;
import org.apache.cxf.transport.http.netty.client.NettyHttpClientPipelineFactory;
import org.apache.cxf.transport.http.netty.client.NettyHttpClientRequest;
import org.apache.cxf.transport.http.netty.client.NettyHttpConduitFactory;
import org.apache.cxf.transport.https.HttpsURLConnectionInfo;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.version.Version;
import org.apache.cxf.ws.addressing.EndpointReferenceType;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.ssl.SslHandler;

public class AbstractNettyHttpConduit extends HttpClientHTTPConduit implements BusLifeCycleListener {
    /**
     * Enable HTTP/2 support
     */
    public static final String ENABLE_HTTP2 = "org.apache.cxf.transports.http2.enabled";
    public static final String MAX_RESPONSE_CONTENT_LENGTH =
            "org.apache.cxf.transport.http.netty.maxResponseContentLength";
    static final Integer DEFAULT_MAX_RESPONSE_CONTENT_LENGTH = 1048576;
    private static final Set KNOWN_HTTP_VERBS_WITH_NO_CONTENT =
            new HashSet<>(Arrays.asList(new String[]{"GET", "HEAD", "OPTIONS", "TRACE"}));

    private final NettyChannelFactory nettyChannelFactory;

    public AbstractNettyHttpConduit(Bus b, EndpointInfo ei, EndpointReferenceType t)
            throws IOException {
        super(b, ei, t);
        this.nettyChannelFactory = b.getExtension(NettyChannelFactory.class);
        if (this.nettyChannelFactory == null) {
            throw new IOException("no NettyChannelFactory in bus");
        }
    }

    public AbstractNettyHttpConduit(Bus b, EndpointInfo ei, EndpointReferenceType t, NettyChannelFactory nettyChannelFactory)
            throws IOException {
        super(b, ei, t);
        this.nettyChannelFactory = nettyChannelFactory;
    }

    public NettyChannelFactory getNettyChannelFactory() {
        return nettyChannelFactory;
    }

    // Using Netty API directly
    protected void setupConnection(Message message, Address address, HTTPClientPolicy csPolicy) throws IOException {
        propagateProtocolSettings(message, csPolicy);

        URI uri = address.getURI();
        boolean addressChanged = false;

        // need to do some clean up work on the URI address
        String uriString = uri.toString();
        if (uriString.startsWith("netty://")) {
            try {
                uriString = uriString.substring(8);
                uri = new URI(uriString);
                addressChanged = true;
            } catch (URISyntaxException ex) {
                throw new MalformedURLException("unsupport uri: " + uriString);
            }
        }
        String s = uri.getScheme();
        if (!"http".equals(s) && !"https".equals(s)) {
            throw new MalformedURLException("unknown protocol: " + s);
        }

        // check tlsClientParameters from message header
        TLSClientParameters clientParameters = message.get(TLSClientParameters.class);
        if (clientParameters == null) {
            clientParameters = tlsClientParameters;
        }

        if (StringUtils.isEmpty(uri.getPath())) {
            //hc needs to have the path be "/"
            uri = uri.resolve("/");
        }

        message.put("http.scheme", uri.getScheme());
        String httpRequestMethod =
                (String) message.get(Message.HTTP_REQUEST_METHOD);
        if (httpRequestMethod == null) {
            httpRequestMethod = "POST";
            message.put(Message.HTTP_REQUEST_METHOD, httpRequestMethod);
        }
        // setup a new NettyHttpClientRequest
        final boolean enableHttp2 = "2.0".equals(csPolicy.getVersion());
        final NettyHttpClientRequest request = new NettyHttpClientRequest(uri, httpRequestMethod, enableHttp2);
        final int ctimeout = determineConnectionTimeout(message, csPolicy);
        final int rtimeout = determineReceiveTimeout(message, csPolicy);
        final int maxResponseContentLength = determineMaxResponseContentLength(message);
        request.setConnectionTimeout(ctimeout);
        request.setReceiveTimeout(rtimeout);
        request.setMaxResponseContentLength(maxResponseContentLength);

        message.put(NettyHttpClientRequest.class, request);

    }

    private void propagateProtocolSettings(Message message, HTTPClientPolicy csPolicy) {
        if (message != null) {
            final Object o = message.getContextualProperty(ENABLE_HTTP2);
            if (o != null) {
                csPolicy.setVersion("2.0");
            }
        }
    }

    protected OutputStream createOutputStream(Message message,
                                              boolean needToCacheRequest,
                                              boolean isChunking,
                                              int chunkThreshold) throws IOException {

        NettyHttpClientRequest entity = message.get(NettyHttpClientRequest.class);
        NettyWrappedOutputStream out = new NettyWrappedOutputStream(message,
                needToCacheRequest,
                isChunking,
                chunkThreshold,
                getConduitName(),
                entity.getUri());
        entity.createRequest(out.getOutBuffer());
        // TODO need to check how to set the Chunked feature
        //request.getRequest().setChunked(true);
        Object contentType = message.get(Message.CONTENT_TYPE);
        if (contentType != null) {
            entity.getRequest().headers().set(Message.CONTENT_TYPE, contentType);
        }
        return out;
    }

    public class NettyWrappedOutputStream extends WrappedOutputStream {
        final HTTPClientPolicy csPolicy;
        final boolean enableHttp2;
        NettyHttpClientRequest entity;
        volatile HttpResponse httpResponse;
        volatile Throwable exception;
        volatile Channel channel;
        volatile SSLSession session;
        boolean isAsync;
        ByteBuf outBuffer;
        OutputStream outputStream;

        final Lock syncLock = new ReentrantLock();
        final Condition connected = syncLock.newCondition();
        final Condition responded = syncLock.newCondition();

        protected NettyWrappedOutputStream(Message message, boolean possibleRetransmit,
                                           boolean isChunking, int chunkThreshold, String conduitName, URI url) {
            super(message, possibleRetransmit, isChunking, chunkThreshold, conduitName, url);
            csPolicy = getClient(message);
            entity = message.get(NettyHttpClientRequest.class);
            int bufSize = csPolicy.getChunkLength() > 0 ? csPolicy.getChunkLength() : 16320;
            outBuffer = Unpooled.buffer(bufSize);
            outputStream = new ByteBufOutputStream(outBuffer);
            enableHttp2 = "2.0".equals(csPolicy.getVersion());
        }

        protected ByteBuf getOutBuffer() {
            return outBuffer;
        }


        protected HttpResponse getHttpResponse() throws IOException {
            syncLock.lock();
            try {
                while (httpResponse == null) {
                    if (exception == null) { //already have an exception, skip waiting
                        try {
                            responded.await(entity.getReceiveTimeout(), TimeUnit.MILLISECONDS);
                        } catch (InterruptedException e) {
                            throw new IOException(e);
                        }
                    }
                    if (httpResponse == null) {

                        if (exception != null) {
                            if (exception instanceof IOException) {
                                throw (IOException) exception;
                            }
                            if (exception instanceof RuntimeException) {
                                throw (RuntimeException) exception;
                            }
                            throw new IOException(exception);
                        }

                        throw new SocketTimeoutException("Read Timeout");
                    }
                }
                return httpResponse;
            } finally {
                syncLock.unlock();
            }
        }

        protected HttpContent getHttpResponseContent() throws IOException {
            return (HttpContent) getHttpResponse();
        }

        protected Channel getChannel() throws IOException {
            syncLock.lock();
            try {
                while (channel == null) {
                    if (exception == null) { //already have an exception, skip waiting
                        try {
                            // connection timeout
                            connected.await(entity.getConnectionTimeout(), TimeUnit.MILLISECONDS);
                        } catch (InterruptedException e) {
                            throw new IOException(e);
                        }
                    }
                    if (channel == null) {

                        if (exception != null) {
                            if (exception instanceof IOException) {
                                throw (IOException) exception;
                            }
                            if (exception instanceof RuntimeException) {
                                throw (RuntimeException) exception;
                            }
                            throw new IOException(exception);
                        }

                        throw new SocketTimeoutException("Connection Timeout");
                    }
                }
                return channel;
            } finally {
                syncLock.unlock();
            }
        }


        @Override
        protected void setupWrappedStream() throws IOException {
            connect(true);
            wrappedStream = new OutputStream() {
                public void write(byte[] b, int off, int len) throws IOException {
                    outputStream.write(b, off, len);
                }

                public void write(int b) throws IOException {
                    outputStream.write(b);
                }

                public void close() throws IOException {
                    // Setup the call back for sending the message
                    ChannelFutureListener listener = new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (!future.isSuccess()) {
                                setException(future.cause());
                            }
                        }
                    };

                    synchronized (entity) {
                        Channel syncChannel = getChannel();
                        ChannelFuture channelFuture = syncChannel.writeAndFlush(entity);
                        channelFuture.addListener(listener);
                        outputStream.close();
                    }
                }
            };

            // If we need to cache for retransmission, store data in a
            // CacheAndWriteOutputStream. Otherwise write directly to the output stream.
            if (cachingForRetransmission) {
                cachedStream = new CacheAndWriteOutputStream(wrappedStream);
                wrappedStream = cachedStream;
            }
        }

        @Override
        protected void handleNoOutput() throws IOException {
            connect(false);
        }

        protected TLSClientParameters findTLSClientParameters() {
            TLSClientParameters clientParameters = outMessage.get(TLSClientParameters.class);
            if (clientParameters == null) {
                clientParameters = getTlsClientParameters();
            }
            if (clientParameters == null) {
                clientParameters = new TLSClientParameters();
            }
            return clientParameters;
        }

        protected void connect(boolean output) {
            final NettyHttpClientPipelineFactory handler;
            if ("https".equals(url.getScheme())) {
                TLSClientParameters clientParameters = findTLSClientParameters();
                handler = new NettyHttpClientPipelineFactory(clientParameters, entity.getReceiveTimeout(),
                        entity.getMaxResponseContentLength(), enableHttp2);
            } else {
                handler = new NettyHttpClientPipelineFactory(null, entity.getReceiveTimeout(),
                        entity.getMaxResponseContentLength(), enableHttp2);
            }

            ChannelFuture connFuture = nettyChannelFactory.connect(bus, url, handler);

            // Setup the call back on the NettyHttpClientRequest
            ChannelFutureListener listener = new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isSuccess()) {
                        setChannel(future.channel());

                        SslHandler sslHandler = channel.pipeline().get(SslHandler.class);

                        if (sslHandler != null) {
                            session = sslHandler.engine().getSession();
                        }
                    } else {
                        setException(future.cause());
                    }
                    synchronized (entity) {
                        //ensure entity is write in main thread
                    }
                }
            };

            connFuture.addListener(listener);

            // setup the CxfResponseCallBack
            CxfResponseCallBack callBack = new CxfResponseCallBack() {
                @Override
                public void responseReceived(HttpResponse response) {
                    setHttpResponse(response);
                }

                @Override
                public void error(Throwable ex) {
                    setException(ex);
                }
            };
            entity.setCxfResponseCallback(callBack);

            if (!output) {
                entity.getRequest().headers().remove("Transfer-Encoding");
                entity.getRequest().headers().remove("Content-Type");

                ChannelFutureListener writeFailureListener = new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            setException(future.cause());
                        }
                    }
                };

                connFuture.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (future.isSuccess()) {
                            handler.whenReady().addListener(new ChannelFutureListener() {
                                @Override
                                public void operationComplete(ChannelFuture future) throws Exception {
                                    if (future.isSuccess()) {
                                        ChannelFuture channelFuture = future.channel().writeAndFlush(entity);
                                        channelFuture.addListener(writeFailureListener);
                                    }
                                }
                            });
                        }
                    }
                });
            }
        }

        @Override
        protected HttpsURLConnectionInfo getHttpsURLConnectionInfo() throws IOException {
            if ("http".equals(outMessage.get("http.scheme"))) {
                return null;
            }
            connect(true);

            HostnameVerifier verifier = org.apache.cxf.transport.https.SSLUtils
                    .getHostnameVerifier(findTLSClientParameters());

            if (!verifier.verify(url.getHost(), session)) {
                throw new IOException("Could not verify host " + url.getHost());
            }

            String method = (String) outMessage.get(Message.HTTP_REQUEST_METHOD);
            String cipherSuite = null;
            Certificate[] localCerts = null;
            Principal principal = null;
            Certificate[] serverCerts = null;
            Principal peer = null;
            if (session != null) {
                cipherSuite = session.getCipherSuite();
                localCerts = session.getLocalCertificates();
                principal = session.getLocalPrincipal();
                serverCerts = session.getPeerCertificates();
                peer = session.getPeerPrincipal();
            }

            return new HttpsURLConnectionInfo(url, method, cipherSuite, localCerts, principal, serverCerts, peer);
        }

        @Override
        protected void setProtocolHeaders() throws IOException {
            Headers h = new Headers(outMessage);
            setContentTypeHeader(h);
            boolean addHeaders = MessageUtils.getContextualBoolean(outMessage, Headers.ADD_HEADERS_PROPERTY, false);

            for (Map.Entry> header : h.headerMap().entrySet()) {
                if (HttpHeaderHelper.CONTENT_TYPE.equalsIgnoreCase(header.getKey())) {
                    continue;
                }
                if (addHeaders || HttpHeaderHelper.COOKIE.equalsIgnoreCase(header.getKey())) {
                    for (String s : header.getValue()) {
                        entity.getRequest().headers().add(HttpHeaderHelper.COOKIE, s);
                    }
                } else if (!"Content-Length".equalsIgnoreCase(header.getKey())) {
                    StringBuilder b = new StringBuilder();
                    for (int i = 0; i < header.getValue().size(); i++) {
                        b.append(header.getValue().get(i));
                        if (i + 1 < header.getValue().size()) {
                            b.append(',');
                        }
                    }
                    entity.getRequest().headers().set(header.getKey(), b.toString());
                }
                if (!entity.getRequest().headers().contains("User-Agent")) {
                    entity.getRequest().headers().set("User-Agent", Version.getCompleteVersionString());
                }
            }
        }

        private void setContentTypeHeader(Headers headers) {
            if (outMessage.get(Message.CONTENT_TYPE) == null) {
                // if no content type is set then check for a request body
                Object requestMethod = outMessage.get(Message.HTTP_REQUEST_METHOD);
                boolean emptyRequest = KNOWN_HTTP_VERBS_WITH_NO_CONTENT.contains(requestMethod)
                        || PropertyUtils.isTrue(outMessage.get(Headers.EMPTY_REQUEST_PROPERTY));
                // If it is not an empty request then add a content type
                if (!emptyRequest) {
                    entity.getRequest().headers().set(Message.CONTENT_TYPE, headers.determineContentType());
                }
            } else {
                entity.getRequest().headers().set(Message.CONTENT_TYPE, headers.determineContentType());
            }
        }

        @Override
        protected void setFixedLengthStreamingMode(int i) {
            // Here we can set the Content-Length
            entity.getRequest().headers().set("Content-Length", i);
            // TODO we need to deal with the Chunked information ourself
            //entity.getRequest().setChunked(false);
        }

        @Override
        protected int getResponseCode() throws IOException {
            return getHttpResponse().status().code();
        }

        @Override
        protected String getResponseMessage() throws IOException {
            return getHttpResponse().status().reasonPhrase();
        }

        @Override
        protected void updateResponseHeaders(Message inMessage) throws IOException {
            Headers h = new Headers(inMessage);
            inMessage.put(Message.CONTENT_TYPE, readHeaders(h));
            cookies.readFromHeaders(h);
        }

        private String readHeaders(Headers h) throws IOException {
            Set headerNames = getHttpResponse().headers().names();
            String ct = null;
            for (String name : headerNames) {
                List s = getHttpResponse().headers().getAll(name);
                h.headerMap().put(name, s);
                if (Message.CONTENT_TYPE.equalsIgnoreCase(name)) {
                    ct = getHttpResponse().headers().get(name);
                }
            }
            return ct;
        }

        @Override
        protected void handleResponseAsync() throws IOException {
            isAsync = true;
        }

        @Override
        protected void closeInputStream() throws IOException {
            //We just clear the buffer
            getHttpResponseContent().content().clear();
        }

        @Override
        protected boolean usingProxy() {
            // TODO we need to support it
            return false;
        }

        @Override
        protected InputStream getInputStream() throws IOException {
            return new ByteBufInputStream(getHttpResponseContent().content());
        }

        @Override
        protected InputStream getPartialResponse() throws IOException {
            InputStream in = null;
            int responseCode = getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_ACCEPTED
                    || responseCode == HttpURLConnection.HTTP_OK) {

                String head = httpResponse.headers().get(HttpHeaderHelper.CONTENT_LENGTH);
                int cli = 0;
                if (head != null) {
                    cli = Integer.parseInt(head);
                }
                head = httpResponse.headers().get(HttpHeaderHelper.TRANSFER_ENCODING);
                boolean isChunked = head != null && HttpHeaderHelper.CHUNKED.equalsIgnoreCase(head);
                head = httpResponse.headers().get(HttpHeaderHelper.CONNECTION);
                boolean isEofTerminated = head != null && HttpHeaderHelper.CLOSE.equalsIgnoreCase(head);
                if (cli > 0) {
                    in = getInputStream();
                } else if (isChunked || isEofTerminated) {
                    // ensure chunked or EOF-terminated response is non-empty
                    try {
                        PushbackInputStream pin =
                                new PushbackInputStream(getInputStream());
                        int c = pin.read();
                        if (c != -1) {
                            pin.unread((byte) c);
                            in = pin;
                        }
                    } catch (IOException ioe) {
                        // ignore
                    }
                }
            }
            return in;
        }

        @Override
        protected void setupNewConnection(String newURL) throws IOException {
            httpResponse = null;
            isAsync = false;
            exception = null;
            if (channel != null) {
                channel.close();
                channel = null;
            }

            try {
                Address address;
                if (defaultAddress.getString().equals(newURL)) {
                    address = defaultAddress;
                    this.url = defaultAddress.getURI();
                } else {
                    this.url = new URI(newURL);
                    address = new Address(newURL, this.url);
                }
                setupConnection(outMessage, address, csPolicy);
                entity = outMessage.get(NettyHttpClientRequest.class);
                //reset the buffers
                outBuffer.clear();
                outBuffer = outBuffer.alloc().buffer(outBuffer.capacity());
                outputStream = new ByteBufOutputStream(outBuffer);
                entity.createRequest(outBuffer);
            } catch (URISyntaxException e) {
                throw new IOException(e);
            }
        }

        @Override
        protected void retransmitStream() throws IOException {
            cachingForRetransmission = false; //already cached
            setupWrappedStream();
            cachedStream.writeCacheTo(wrappedStream);
            wrappedStream.flush();
            wrappedStream.close();
        }

        @Override
        protected void updateCookiesBeforeRetransmit() throws IOException {
            Headers h = new Headers();
            readHeaders(h);
            cookies.readFromHeaders(h);
        }

        @Override
        public void thresholdReached() throws IOException {
            //TODO need to support the chunked version
            //entity.getRequest().setChunked(true);
        }

        protected void setHttpResponse(HttpResponse r) {
            syncLock.lock();
            try {
                httpResponse = r;
                if (isAsync) {
                    //got a response, need to start the response processing now
                    try {
                        handleResponseOnWorkqueue(false, true);
                        isAsync = false; // don't trigger another start on next block. :-)
                    } catch (Exception ex) {
                        //ignore, we'll try again on the next consume;
                    }
                }
                responded.signalAll();
            } finally {
                syncLock.unlock();
            }
        }

        protected void setException(Throwable ex) {
            syncLock.lock();
            try {
                exception = ex;
                if (isAsync) {
                    //got a response, need to start the response processing now
                    try {
                        handleResponseOnWorkqueue(false, true);
                        isAsync = false; // don't trigger another start on next block. :-)
                    } catch (Exception ex2) {
                        ex2.printStackTrace();
                    }
                }
                responded.signalAll();
            } finally {
                syncLock.unlock();
            }
        }

        protected void setChannel(Channel ch) {
            syncLock.lock();
            try {
                channel = ch;
                connected.signalAll();
            } finally {
                syncLock.unlock();
            }
        }
    }

    @Override
    public void initComplete() {

    }

    @Override
    public void postShutdown() {
        // shutdown the conduit
        this.close();

    }

    @Override
    public void preShutdown() {
    }

    protected static int determineMaxResponseContentLength(Message message) {
        Integer maxResponseContentLength = null;
        if (message.get(MAX_RESPONSE_CONTENT_LENGTH) != null) {
            Object obj = message.get(MAX_RESPONSE_CONTENT_LENGTH);
            try {
                maxResponseContentLength = Integer.parseInt(obj.toString());
            } catch (NumberFormatException e) {
                LOG.log(Level.WARNING, "INVALID_TIMEOUT_FORMAT", new Object[]{
                        MAX_RESPONSE_CONTENT_LENGTH, obj.toString()
                });
            }
        }
        if (maxResponseContentLength == null) {
            maxResponseContentLength = DEFAULT_MAX_RESPONSE_CONTENT_LENGTH;
        }
        return maxResponseContentLength;
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy