/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.synapse.transport.nhttp.config;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axis2.AxisFault;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.transport.base.ParamUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.params.HttpParams;
import org.apache.synapse.transport.certificatevalidation.RevocationVerificationManager;
import org.apache.synapse.transport.http.conn.ClientConnFactory;
import org.apache.synapse.transport.http.conn.ClientSSLSetupHandler;
import org.apache.synapse.transport.http.conn.SSLContextDetails;
import org.apache.synapse.transport.nhttp.NoValidateCertTrustManager;
public class ClientConnFactoryBuilder {
private static final Log log = LogFactory.getLog(ClientConnFactoryBuilder.class);
private final TransportOutDescription transportOut;
private final String name;
private SSLContextDetails ssl = null;
private Map<String, SSLContext> sslByHostMap = null;
public ClientConnFactoryBuilder(final TransportOutDescription transportOut) {
super();
this.transportOut = transportOut;
this.name = transportOut.getName().toUpperCase(Locale.US);
}
public ClientConnFactoryBuilder parseSSL() throws AxisFault {
Parameter keyParam = transportOut.getParameter("keystore");
Parameter trustParam = transportOut.getParameter("truststore");
Parameter httpsProtocolsParam = transportOut.getParameter("HttpsProtocols");
OMElement ksEle = null;
OMElement tsEle = null;
if (keyParam != null) {
ksEle = keyParam.getParameterElement().getFirstElement();
}
boolean novalidatecert = ParamUtils.getOptionalParamBoolean(transportOut,
"novalidatecert", false);
if (trustParam != null) {
if (novalidatecert) {
if (log.isWarnEnabled()) {
log.warn(name + " Ignoring novalidatecert parameter since a truststore has been specified");
}
}
tsEle = trustParam.getParameterElement().getFirstElement();
}
SSLContext sslContext = createSSLContext(ksEle, tsEle, novalidatecert);
final Parameter hvp = transportOut.getParameter("HostnameVerifier");
final String hvs = hvp != null ? hvp.getValue().toString() : null;
final X509HostnameVerifier hostnameVerifier;
if ("Strict".equalsIgnoreCase(hvs)) {
hostnameVerifier = ClientSSLSetupHandler.STRICT;
} else if ("AllowAll".equalsIgnoreCase(hvs)) {
hostnameVerifier = ClientSSLSetupHandler.ALLOW_ALL;
} else if ("DefaultAndLocalhost".equalsIgnoreCase(hvs)) {
hostnameVerifier = ClientSSLSetupHandler.DEFAULT_AND_LOCALHOST;
} else {
hostnameVerifier = ClientSSLSetupHandler.DEFAULT;
}
final Parameter cvp = transportOut.getParameter("CertificateRevocationVerifier");
final String cvEnable = cvp != null ?
cvp.getParameterElement().getAttribute(new QName("enable")).getAttributeValue() : null;
RevocationVerificationManager revocationVerifier = null;
if ("true".equalsIgnoreCase(cvEnable)) {
String cacheSizeString = cvp.getParameterElement().getFirstChildWithName(new QName("CacheSize")).getText();
String cacheDelayString = cvp.getParameterElement().getFirstChildWithName(new QName("CacheDelay")).getText();
Integer cacheSize = null;
Integer cacheDelay = null;
try {
cacheSize = new Integer(cacheSizeString);
cacheDelay = new Integer(cacheDelayString);
} catch (NumberFormatException e) {
}
revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay);
}
// Process HttpProtocols
OMElement httpsProtocolsEl = httpsProtocolsParam != null ? httpsProtocolsParam.getParameterElement() : null;
String[] httpsProtocols = null;
final String configuredHttpsProtocols =
httpsProtocolsEl != null ? httpsProtocolsEl.getText() : null;
if (configuredHttpsProtocols != null && configuredHttpsProtocols.trim().length() != 0) {
String[] configuredValues = configuredHttpsProtocols.trim().split(",");
List<String> protocolList = new ArrayList<String>(configuredValues.length);
for (String protocol : configuredValues) {
if (!protocol.trim().isEmpty()) {
protocolList.add(protocol.trim());
}
}
httpsProtocols = protocolList.toArray(new String[protocolList.size()]);
}
// Initiated separately to cater setting https protocols
ClientSSLSetupHandler clientSSLSetupHandler = new ClientSSLSetupHandler(hostnameVerifier, revocationVerifier);
if (null != httpsProtocols) {
clientSSLSetupHandler.setHttpsProtocols(httpsProtocols);
}
ssl = new SSLContextDetails(sslContext, clientSSLSetupHandler);
sslByHostMap = getCustomSSLContexts(transportOut);
return this;
}
/**
* Looks for a transport parameter named customSSLProfiles and initializes zero or more
* custom SSLContext instances. The syntax for defining custom SSL profiles is as follows.
*
* <parameter name="customSSLProfiles>
* <profile>
* <servers>www.test.org:80, www.test2.com:9763</servers>
* <KeyStore>
* <Location>/path/to/identity/store</Location>
* <Type>JKS</Type>
* <Password>password</Password>
* <KeyPassword>password</KeyPassword>
* </KeyStore>
* <TrustStore>
* <Location>path/tp/trust/store</Location>
* <Type>JKS</Type>
* <Password>password</Password>
* </TrustStore>
* </profile>
* </parameter>
*
* Any number of profiles can be defined under the customSSLProfiles parameter.
*
* @param transportOut transport out description
* @return a map of server addresses and SSL contexts
* @throws AxisFault if at least on SSL profile is not properly configured
*/
private Map<String, SSLContext> getCustomSSLContexts(TransportOutDescription transportOut)
throws AxisFault {
TransportOutDescription customSSLProfileTransport = loadDynamicSSLConfig(transportOut);
Parameter customProfilesParam = customSSLProfileTransport.getParameter("customSSLProfiles");
if (customProfilesParam == null) {
return null;
}
if (log.isInfoEnabled()) {
log.info(name + " Loading custom SSL profiles for the HTTPS sender");
}
OMElement customProfilesElt = customProfilesParam.getParameterElement();
Iterator<?> profiles = customProfilesElt.getChildrenWithName(new QName("profile"));
Map<String, SSLContext> contextMap = new HashMap<String, SSLContext>();
while (profiles.hasNext()) {
OMElement profile = (OMElement) profiles.next();
OMElement serversElt = profile.getFirstChildWithName(new QName("servers"));
if (serversElt == null || serversElt.getText() == null) {
String msg = "Each custom SSL profile must define at least one host:port " +
"pair under the servers element";
log.error(name + " " + msg);
throw new AxisFault(msg);
}
String[] servers = serversElt.getText().split(",");
OMElement ksElt = profile.getFirstChildWithName(new QName("KeyStore"));
OMElement trElt = profile.getFirstChildWithName(new QName("TrustStore"));
String noValCert = profile.getAttributeValue(new QName("novalidatecert"));
boolean novalidatecert = "true".equals(noValCert);
SSLContext sslContext = createSSLContext(ksElt, trElt, novalidatecert);
for (String server : servers) {
server = server.trim();
if (!contextMap.containsKey(server)) {
contextMap.put(server, sslContext);
} else {
if (log.isWarnEnabled()) {
log.warn(name + " Multiple SSL profiles were found for the server : " +
server + ". Ignoring the excessive profiles.");
}
}
}
}
if (contextMap.size() > 0) {
if (log.isInfoEnabled()) {
log.info(name + " Custom SSL profiles initialized for " + contextMap.size() +
" servers");
}
return contextMap;
}
return null;
}
private SSLContext createSSLContext(OMElement keyStoreElt, OMElement trustStoreElt,
boolean novalidatecert) throws AxisFault {
KeyManager[] keymanagers = null;
TrustManager[] trustManagers = null;
if (keyStoreElt != null) {
String location = keyStoreElt.getFirstChildWithName(new QName("Location")).getText();
String type = keyStoreElt.getFirstChildWithName(new QName("Type")).getText();
String storePassword = keyStoreElt.getFirstChildWithName(new QName("Password")).getText();
String keyPassword = keyStoreElt.getFirstChildWithName(new QName("KeyPassword")).getText();
FileInputStream fis = null;
try {
KeyStore keyStore = KeyStore.getInstance(type);
fis = new FileInputStream(location);
if (log.isInfoEnabled()) {
log.info(name + " Loading Identity Keystore from : " + location);
}
keyStore.load(fis, storePassword.toCharArray());
KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmfactory.init(keyStore, keyPassword.toCharArray());
keymanagers = kmfactory.getKeyManagers();
} catch (GeneralSecurityException gse) {
log.error(name + " Error loading Keystore : " + location, gse);
throw new AxisFault("Error loading Keystore : " + location, gse);
} catch (IOException ioe) {
log.error(name + " Error opening Keystore : " + location, ioe);
throw new AxisFault("Error opening Keystore : " + location, ioe);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignore) {}
}
}
}
if (trustStoreElt != null) {
if (novalidatecert && log.isWarnEnabled()) {
log.warn(name + " Ignoring novalidatecert parameter since a truststore has been specified");
}
String location = trustStoreElt.getFirstChildWithName(new QName("Location")).getText();
String type = trustStoreElt.getFirstChildWithName(new QName("Type")).getText();
String storePassword = trustStoreElt.getFirstChildWithName(new QName("Password")).getText();
FileInputStream fis = null;
try {
KeyStore trustStore = KeyStore.getInstance(type);
fis = new FileInputStream(location);
if (log.isInfoEnabled()) {
log.info(name + " Loading Trust Keystore from : " + location);
}
trustStore.load(fis, storePassword.toCharArray());
TrustManagerFactory trustManagerfactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerfactory.init(trustStore);
trustManagers = trustManagerfactory.getTrustManagers();
} catch (GeneralSecurityException gse) {
log.error(name + " Error loading Key store : " + location, gse);
throw new AxisFault("Error loading Key store : " + location, gse);
} catch (IOException ioe) {
log.error(name + " Error opening Key store : " + location, ioe);
throw new AxisFault("Error opening Key store : " + location, ioe);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignore) {}
}
}
} else if (novalidatecert) {
if (log.isWarnEnabled()) {
log.warn(name + " Server certificate validation (trust) has been disabled. " +
"DO NOT USE IN PRODUCTION!");
}
trustManagers = new TrustManager[] { new NoValidateCertTrustManager() };
}
try {
final Parameter sslpParameter = transportOut.getParameter("SSLProtocol");
final String sslProtocol = sslpParameter != null ? sslpParameter.getValue().toString() : "TLS";
SSLContext sslcontext = SSLContext.getInstance(sslProtocol);
sslcontext.init(keymanagers, trustManagers, null);
return sslcontext;
} catch (GeneralSecurityException gse) {
log.error(name + " Unable to create SSL context with the given configuration", gse);
throw new AxisFault("Unable to create SSL context with the given configuration", gse);
}
}
public ClientConnFactory createConnFactory(final HttpParams params) {
if (ssl != null) {
return new ClientConnFactory(ssl, sslByHostMap, params);
} else {
return new ClientConnFactory(params);
}
}
/**
* Extracts Dynamic SSL profiles configuration from given TransportOut Configuration
*
* @param transportOut TransportOut Configuration of the connection
* @return TransportOut configuration with extracted Dynamic SSL profiles information
*/
public TransportOutDescription loadDynamicSSLConfig (TransportOutDescription transportOut) {
Parameter profilePathParam = transportOut.getParameter("dynamicSSLProfilesConfig");
//No Separate configuration file configured. Therefore using Axis2 Configuration
if (profilePathParam == null) {
return transportOut;
}
//Using separate SSL Profile configuration file, ignore Axis2 configurations
OMElement pathEl = profilePathParam.getParameterElement();
String path = pathEl.getFirstChildWithName(new QName("filePath")).getText();
try {
if (path != null) {
String separator = path.startsWith(System.getProperty("file.separator")) ?
"" : System.getProperty("file.separator");
String fullPath = System.getProperty("user.dir") + separator + path;
OMElement profileEl = new StAXOMBuilder(fullPath).getDocumentElement();
Parameter profileParam = new Parameter();
profileParam.setParameterElement(profileEl);
profileParam.setName("customSSLProfiles");
profileParam.setValue(profileEl);
transportOut.addParameter(profileParam);
log.info("customSSLProfiles configuration is loaded from path: " + fullPath);
return transportOut;
}
} catch (XMLStreamException xmlEx) {
log.error("XMLStreamException - Could not load customSSLProfiles from file path: " + path, xmlEx);
} catch (FileNotFoundException fileEx) {
log.error("FileNotFoundException - Could not load customSSLProfiles from file path: " + path, fileEx);
} catch (AxisFault axisFault) {
log.error("AxisFault - Could not load customSSLProfiles from file path: " + path, axisFault);
} catch (Exception ex) {
log.error("Exception - Could not load customSSLProfiles from file path: " + path, ex);
}
return null;
}
}