/* 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; } } }