org.dcache.srm.client.HttpClientSender.java Source code

Java tutorial

Introduction

Here is the source code for org.dcache.srm.client.HttpClientSender.java

Source

/* dCache - http://www.dcache.org/
 *
 * Copyright (C) 2015 Deutsches Elektronen-Synchrotron
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Copyright 2001-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.dcache.srm.client;

import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.components.net.CommonsHTTPClientProperties;
import org.apache.axis.components.net.CommonsHTTPClientPropertiesFactory;
import org.apache.axis.handlers.BasicHandler;
import org.apache.axis.soap.SOAP12Constants;
import org.apache.axis.soap.SOAPConstants;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.axis.utils.JavaUtils;
import org.apache.axis.utils.NetworkUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.GzipCompressingEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.HostnameVerifier;
import javax.xml.soap.MimeHeader;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;

import org.dcache.ssl.SslContextFactory;
import org.dcache.util.Version;

/**
 * This class provides Apache Commons's HTTP components client support for Axis 1.
 *
 * Based on org.apache.axis.transport.http.CommonsHTTPSender. Use in combination with
 * HttpClientTransport.  In contrast to the transport, the handler is only instantiated
 * once and cannot maintain state for a particular connection.
 *
 * @author Davanum Srinivas (dims@yahoo.com)
 * History: By Chandra Talluri
 *          Modifications done for maintaining sessions. Cookies needed to be set on
 *          HttpState not on MessageContext, since ttpMethodBase overwrites the cookies
 *          from HttpState. Also we need to setCookiePolicy on HttpState to
 *          CookiePolicy.COMPATIBILITY else it is defaulting to RFC2109Spec and adding
 *          Version information to it and tomcat server not recognizing it
 *
 *          By Gerd Behrmann (behrmann@ndgf.org)
 *          Ported to Apache Common's HTTP components client. Does not support HTTP proxies.
 */
public class HttpClientSender extends BasicHandler {
    protected static final Logger LOGGER = LoggerFactory.getLogger(HttpClientSender.class);

    public static final Version VERSION = Version.of(HttpClientSender.class);

    private static final long serialVersionUID = -5237082853330993915L;

    protected CommonsHTTPClientProperties clientProperties;
    protected CloseableHttpClient httpClient;

    protected String[] supportedProtocols;
    protected String[] supportedCipherSuites;
    protected SslContextFactory sslContextFactory;
    protected HostnameVerifier hostnameVerifier;

    public String[] getSupportedProtocols() {
        return supportedProtocols;
    }

    public void setSupportedProtocols(String[] supportedProtocols) {
        this.supportedProtocols = supportedProtocols;
    }

    public String[] getSupportedCipherSuites() {
        return supportedCipherSuites;
    }

    public void setSupportedCipherSuites(String[] supportedCipherSuites) {
        this.supportedCipherSuites = supportedCipherSuites;
    }

    public SslContextFactory getSslContextFactory() {
        return sslContextFactory;
    }

    public void setSslContextFactory(SslContextFactory sslContextFactory) {
        this.sslContextFactory = sslContextFactory;
    }

    public HostnameVerifier getHostnameVerifier() {
        return hostnameVerifier;
    }

    public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
        this.hostnameVerifier = hostnameVerifier;
    }

    @Override
    public void init() {
        clientProperties = CommonsHTTPClientPropertiesFactory.create();
        httpClient = createHttpClient(createConnectionManager());
    }

    @Override
    public void cleanup() {
        try {
            httpClient.close();
            httpClient = null;
        } catch (IOException e) {
            throw new RuntimeException("Failed to close HTTP client: " + e.getMessage(), e);
        }
    }

    /**
     * Creates the registries of socket factories to be used to establish connection to SOAP servers.
     */
    protected Registry<ConnectionSocketFactory> createSocketFactoryRegistry() {
        return RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", new FlexibleCredentialSSLConnectionSocketFactory(sslContextFactory,
                        supportedProtocols, supportedCipherSuites, hostnameVerifier))
                .build();
    }

    /**
     * Creates the connection manager to be used to manage connections to SOAP servers.
     */
    protected PoolingHttpClientConnectionManager createConnectionManager() {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
                createSocketFactoryRegistry());
        cm.setMaxTotal(clientProperties.getMaximumTotalConnections());
        cm.setDefaultMaxPerRoute(clientProperties.getMaximumConnectionsPerHost());
        SocketConfig.Builder socketOptions = SocketConfig.custom();
        if (clientProperties.getDefaultSoTimeout() > 0) {
            socketOptions.setSoTimeout(clientProperties.getDefaultSoTimeout());
        }
        cm.setDefaultSocketConfig(socketOptions.build());
        return cm;
    }

    /**
     * Creates the HttpClient used to submit SOAP requests.
     */
    protected CloseableHttpClient createHttpClient(PoolingHttpClientConnectionManager connectionManager) {
        return HttpClients.custom().setConnectionManager(connectionManager)
                .setUserAgent("dCache/" + VERSION.getVersion())
                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()).build();
    }

    /**
     * Creates the HttpContext for a particular call to a SOAP server.
     *
     * Called once per session.
     */
    protected HttpClientContext createHttpContext(MessageContext msgContext, URI uri) {
        HttpClientContext context = new HttpClientContext(new BasicHttpContext());
        // if UserID is not part of the context, but is in the URL, use
        // the one in the URL.
        String userID = msgContext.getUsername();
        String passwd = msgContext.getPassword();
        if ((userID == null) && (uri.getUserInfo() != null)) {
            String info = uri.getUserInfo();
            int sep = info.indexOf(':');
            if ((sep >= 0) && (sep + 1 < info.length())) {
                userID = info.substring(0, sep);
                passwd = info.substring(sep + 1);
            } else {
                userID = info;
            }
        }
        if (userID != null) {
            CredentialsProvider credsProvider = new BasicCredentialsProvider();
            // if the username is in the form "user\domain"
            // then use NTCredentials instead.
            int domainIndex = userID.indexOf('\\');
            if (domainIndex > 0 && userID.length() > domainIndex + 1) {
                String domain = userID.substring(0, domainIndex);
                String user = userID.substring(domainIndex + 1);
                credsProvider.setCredentials(AuthScope.ANY,
                        new NTCredentials(user, passwd, NetworkUtils.getLocalHostname(), domain));
            } else {
                credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userID, passwd));
            }
            context.setCredentialsProvider(credsProvider);
        }
        context.setAttribute(HttpClientTransport.TRANSPORT_HTTP_CREDENTIALS,
                msgContext.getProperty(HttpClientTransport.TRANSPORT_HTTP_CREDENTIALS));
        return context;
    }

    /**
     * Creates a HttpRequest encoding a particular SOAP call.
     *
     * Called once per SOAP call.
     */
    protected HttpUriRequest createHttpRequest(MessageContext msgContext, URI url) throws AxisFault {
        boolean posting = true;
        // If we're SOAP 1.2, allow the web method to be set from the
        // MessageContext.
        if (msgContext.getSOAPConstants() == SOAPConstants.SOAP12_CONSTANTS) {
            String webMethod = msgContext.getStrProp(SOAP12Constants.PROP_WEBMETHOD);
            if (webMethod != null) {
                posting = webMethod.equals(HTTPConstants.HEADER_POST);
            }
        }

        HttpRequestBase request = posting ? new HttpPost(url) : new HttpGet(url);

        // Get SOAPAction, default to ""
        String action = msgContext.useSOAPAction() ? msgContext.getSOAPActionURI() : "";
        if (action == null) {
            action = "";
        }

        Message msg = msgContext.getRequestMessage();
        request.addHeader(HTTPConstants.HEADER_CONTENT_TYPE, msg.getContentType(msgContext.getSOAPConstants()));
        request.addHeader(HTTPConstants.HEADER_SOAP_ACTION, "\"" + action + "\"");

        String httpVersion = msgContext.getStrProp(MessageContext.HTTP_TRANSPORT_VERSION);
        if (httpVersion != null && httpVersion.equals(HTTPConstants.HEADER_PROTOCOL_V10)) {
            request.setProtocolVersion(HttpVersion.HTTP_1_0);
        }

        // Transfer MIME headers of SOAPMessage to HTTP headers.
        MimeHeaders mimeHeaders = msg.getMimeHeaders();
        if (mimeHeaders != null) {
            Iterator i = mimeHeaders.getAllHeaders();
            while (i.hasNext()) {
                MimeHeader mimeHeader = (MimeHeader) i.next();
                // HEADER_CONTENT_TYPE and HEADER_SOAP_ACTION are already set.
                // Let's not duplicate them.
                String name = mimeHeader.getName();
                if (!name.equals(HTTPConstants.HEADER_CONTENT_TYPE)
                        && !name.equals(HTTPConstants.HEADER_SOAP_ACTION)) {
                    request.addHeader(name, mimeHeader.getValue());
                }
            }
        }

        boolean isChunked = false;
        boolean isExpectContinueEnabled = false;
        Map<?, ?> userHeaderTable = (Map) msgContext.getProperty(HTTPConstants.REQUEST_HEADERS);
        if (userHeaderTable != null) {
            for (Map.Entry<?, ?> me : userHeaderTable.entrySet()) {
                Object keyObj = me.getKey();
                if (keyObj != null) {
                    String key = keyObj.toString().trim();
                    String value = me.getValue().toString().trim();
                    if (key.equalsIgnoreCase(HTTPConstants.HEADER_EXPECT)) {
                        isExpectContinueEnabled = value.equalsIgnoreCase(HTTPConstants.HEADER_EXPECT_100_Continue);
                    } else if (key.equalsIgnoreCase(HTTPConstants.HEADER_TRANSFER_ENCODING_CHUNKED)) {
                        isChunked = JavaUtils.isTrue(value);
                    } else {
                        request.addHeader(key, value);
                    }
                }
            }
        }

        RequestConfig.Builder config = RequestConfig.custom();
        // optionally set a timeout for the request
        if (msgContext.getTimeout() != 0) {
            /* ISSUE: these are not the same, but MessageContext has only one definition of timeout */
            config.setSocketTimeout(msgContext.getTimeout()).setConnectTimeout(msgContext.getTimeout());
        } else if (clientProperties.getConnectionPoolTimeout() != 0) {
            config.setConnectTimeout(clientProperties.getConnectionPoolTimeout());
        }
        config.setContentCompressionEnabled(msgContext.isPropertyTrue(HTTPConstants.MC_ACCEPT_GZIP));
        config.setExpectContinueEnabled(isExpectContinueEnabled);
        request.setConfig(config.build());

        if (request instanceof HttpPost) {
            HttpEntity requestEntity = new MessageEntity(request, msgContext.getRequestMessage(), isChunked);
            if (msgContext.isPropertyTrue(HTTPConstants.MC_GZIP_REQUEST)) {
                requestEntity = new GzipCompressingEntity(requestEntity);
            }
            ((HttpPost) request).setEntity(requestEntity);
        }

        return request;
    }

    /**
     * Extracts the SOAP response from an HttpResponse.
     */
    protected Message extractResponse(MessageContext msgContext, HttpResponse response) throws IOException {
        int returnCode = response.getStatusLine().getStatusCode();
        HttpEntity entity = response.getEntity();
        if (entity != null && returnCode > 199 && returnCode < 300) {
            // SOAP return is OK - so fall through
        } else if (entity != null && msgContext.getSOAPConstants() == SOAPConstants.SOAP12_CONSTANTS) {
            // For now, if we're SOAP 1.2, fall through, since the range of
            // valid result codes is much greater
        } else if (entity != null && returnCode > 499 && returnCode < 600
                && Objects.equals(getMimeType(entity), "text/xml")) {
            // SOAP Fault should be in here - so fall through
        } else {
            String statusMessage = response.getStatusLine().getReasonPhrase();
            AxisFault fault = new AxisFault("HTTP", "(" + returnCode + ")" + statusMessage, null, null);
            fault.setFaultDetailString("Return code: " + String.valueOf(returnCode)
                    + (entity == null ? "" : "\n" + EntityUtils.toString(entity)));
            fault.addFaultDetail(Constants.QNAME_FAULTDETAIL_HTTPERRORCODE, String.valueOf(returnCode));
            throw fault;
        }

        Header contentLocation = response.getFirstHeader(HttpHeaders.CONTENT_LOCATION);
        Message outMsg = new Message(entity.getContent(), false, Objects.toString(ContentType.get(entity), null),
                (contentLocation == null) ? null : contentLocation.getValue());
        // Transfer HTTP headers of HTTP message to MIME headers of SOAP message
        MimeHeaders responseMimeHeaders = outMsg.getMimeHeaders();
        for (Header responseHeader : response.getAllHeaders()) {
            responseMimeHeaders.addHeader(responseHeader.getName(), responseHeader.getValue());
        }
        outMsg.setMessageType(Message.RESPONSE);
        return outMsg;
    }

    private static String getMimeType(HttpEntity entity) {
        ContentType contentType = ContentType.get(entity);
        return (contentType == null) ? null : contentType.getMimeType();
    }

    /**
     * Sends the request SOAP message and then reads the response SOAP message back from the SOAP server.
     */
    @Override
    public void invoke(MessageContext msgContext) throws AxisFault {
        try {
            URI uri = new URI(msgContext.getStrProp(MessageContext.TRANS_URL));

            HttpClientContext context;
            if (msgContext.getMaintainSession()) {
                context = (HttpClientContext) msgContext.getProperty(HttpClientTransport.TRANSPORT_HTTP_CONTEXT);
                if (context == null) {
                    context = createHttpContext(msgContext, uri);
                    msgContext.setProperty(HttpClientTransport.TRANSPORT_HTTP_CONTEXT, context);
                }
            } else {
                context = createHttpContext(msgContext, uri);
            }

            HttpUriRequest request = createHttpRequest(msgContext, uri);
            try (CloseableHttpResponse response = httpClient.execute(request, context)) {
                msgContext.setPastPivot(true);
                Message outMsg = extractResponse(msgContext, response);
                msgContext.setResponseMessage(outMsg);
                outMsg.getSOAPEnvelope();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(outMsg.getSOAPPartAsString());
                }
            }
        } catch (AxisFault e) {
            LOGGER.debug("SOAP invocation failed: {}", e.toString());
            throw e;
        } catch (IOException | URISyntaxException e) {
            LOGGER.debug("SOAP invocation failed: {}", e.toString());
            throw AxisFault.makeFault(e);
        }
    }

    protected static class MessageEntity extends AbstractHttpEntity {
        private final HttpRequestBase method;
        private final Message message;

        public MessageEntity(HttpRequestBase method, Message message, boolean httpChunkStream) {
            this.message = message;
            this.method = method;
            setChunked(httpChunkStream);
        }

        protected boolean isContentLengthNeeded() {
            return method.getProtocolVersion().equals(HttpVersion.HTTP_1_0) || !isChunked();
        }

        @Override
        public boolean isRepeatable() {
            return true;
        }

        @Override
        public long getContentLength() {
            if (isContentLengthNeeded()) {
                try {
                    return message.getContentLength();
                } catch (AxisFault ignored) {
                }
            }
            return -1;
        }

        @Override
        public InputStream getContent() throws IOException, UnsupportedOperationException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void writeTo(OutputStream outstream) throws IOException {
            try {
                message.writeTo(outstream);
            } catch (SOAPException e) {
                throw new IOException(e.getMessage(), e);
            }
        }

        @Override
        public boolean isStreaming() {
            return false;
        }
    }
}