/*
* Copyright (c) 2012 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.cimadapter.connections.cim;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.BindException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import javax.cim.CIMInstance;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.wbem.listener.IndicationListener;
import javax.wbem.listener.WBEMListener;
import javax.wbem.listener.WBEMListenerFactory;
import org.sblim.cimclient.WBEMConfigurationProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.cimadapter.connections.ConnectionManagerException;
import com.emc.storageos.cimadapter.consumers.CimIndicationConsumer;
import com.emc.storageos.cimadapter.consumers.CimIndicationConsumerList;
import com.emc.storageos.cimadapter.processors.CimIndicationProcessor;
import com.emc.storageos.cimadapter.processors.CimIndicationSet;
import com.emc.storageos.services.ServicesConstants;
/**
* CIM indication listener that hands off processing of each received indication
* to the {@link CimConnection} that subscribed to that indication. If a
* matching connection cannot be found, the indication is discarded.
*/
public class CimListener implements IndicationListener {
// Flag indicates if the listener is running.
private boolean _isRunning = false;
// Flag indicates if the listener is paused.
private volatile boolean _isPaused = false;
// The URL for the listener.
private URL _url;
// A reference to the contained WBEM listener.
private WBEMListener _listener;
// The CIM indication queue.
private Queue<CimQueuedIndication> _queue;
// A map of the connection associated with the listener key'd by name.
private ConcurrentMap<String, CimConnection> _connections;
// A reference to the list of event consumers to which published indications
// are sent.
private CimIndicationConsumerList _indicationConsumers = null;
// A logger reference.
private static final Logger s_logger = LoggerFactory.getLogger(CimListener.class);
private int defaultSMISSSLPort;
// keystore and trust store locations
private static String _keystoreLocation = System.getProperty(ServicesConstants.KEYSTORE_BASE_PATH_VARIABLE) +
ServicesConstants.KEYSTORE_FILE_NAME;
private static String _trustStoreLocation = System.getProperty(ServicesConstants.TRUSTSTORE_BASE_PATH_VARIABLE) +
ServicesConstants.TRUSTSTORE_FILE_NAME;
/**
* Indications's thread pool to handle indications from smis.
* Executors.newFixedThreadPool will give unbound thread pool.
* So no need to bother about RejectedExecutionHandler here.
*/
private ExecutorService executorService = Executors.newFixedThreadPool(50);
/**
* Constructs a listener given the passed configuration.
*
* @param info The listener configuration extracted from the Spring
* configuration file.
* @param indicationConsumers The list of consumers to receive published
* events.
*/
public CimListener(CimListenerInfo info, CimIndicationConsumerList indicationConsumers) {
String hostIP = info.getHostIP();
if ((hostIP != null) && (hostIP.length() != 0)) {
String protocol = info.getProtocol();
int port = info.getPort();
try {
_url = new URL(protocol, hostIP, port, "");
} catch (Exception e) {
s_logger.error("Error forming listener URL. Indications will not be received.", e);
}
} else {
s_logger.error("Could not determine listener host. Indications will not be received.");
}
defaultSMISSSLPort = info.getDefaultSMISSSLPort();
int queueSize = info.getQueueSize();
_queue = new LinkedBlockingQueue<CimQueuedIndication>(queueSize);
_connections = new ConcurrentHashMap<String, CimConnection>();
_indicationConsumers = indicationConsumers;
}
/**
* Getter for the listener's URL.
*
* @return The listener's URL.
*/
public URL getURL() {
return _url;
}
/**
* Registers a CIM connection with the listener. This allows the listener to
* match incoming indications from that connection's subscriptions. This
* assumes that the subscriptions are set up to include the connection name
* in the destination URL.
*
* @param connection The CIM connection to be registered.
*/
public synchronized void register(CimConnection connection) {
// For some reason, SBLIM CIM client forces the path in
// indication URLs to lower case. This "bug" has been
// reported, but hasn't been fixed as of version 2.1.8.
// To match indications with connections, names must be
// normalized to lowercase.
String key = connection.getConnectionName().toLowerCase();
_connections.put(key, connection);
s_logger.info("Registered {}", connection.getConnectionName());
}
/**
* Unregisters the given CIM connection.
*
* @param connection The CIM connection to be unregistered.
*/
public synchronized void unregister(CimConnection connection) {
// TBD Is this a bug? The name is not forced to lowercase as in the
// register method.
_connections.remove(connection.getConnectionName());
s_logger.info("Unregistered {}", connection.getConnectionName());
}
/**
* Starts listening.
*
* @throws IOException
*/
public synchronized void startup() throws IOException {
// Only start the listener if the host URL has been set.
if (_url != null) {
while (_listener == null) {
s_logger.info("Starting listener at {}", _url);
_listener = WBEMListenerFactory.getListener(CimConstants.CIM_CLIENT_PROTOCOL);
// It can take a few attempts to reacquire the TCP port immediately
// after releasing it.
try {
String ecomProtocol = _url.getProtocol();
s_logger.info("ecomProtocol: {}", ecomProtocol);
if ("https".equalsIgnoreCase(ecomProtocol)) {
s_logger.info("Setting up secure listener port");
_listener.setProperty(WBEMConfigurationProperties.KEYSTORE_PATH, _keystoreLocation);
s_logger.info("keystore location: {}", _keystoreLocation);
_listener.setProperty(WBEMConfigurationProperties.KEYSTORE_PASSWORD, "changeit");
// truststore
_listener.setProperty(WBEMConfigurationProperties.SSL_LISTENER_PEER_VERIFICATION, "require");
_listener.setProperty(WBEMConfigurationProperties.TRUSTSTORE_PATH, _trustStoreLocation);
_listener.setProperty(WBEMConfigurationProperties.TRUSTSTORE_PASSWORD, "changeit");
s_logger.info("Enabled secure listener port");
} else {
s_logger.info("Enabled non-secure listener port");
}
_listener.addListener(this, _url.getPort(), ecomProtocol);
} catch (BindException e) {
s_logger.error("Failed binding CIM listener", e);
try {
Thread.sleep(CimConstants.LISTENER_RETRY_INTERVAL);
} catch (InterruptedException ie) {
s_logger.error(ie.getMessage(), ie);
}
}
}
s_logger.info("Listening at {}", _url);
_isRunning = true;
_isPaused = false;
}
else {
s_logger.error("Can't start listener. The host URL is not set.");
}
}
/**
* Forwards CIM indications to a matching connection. This only works if the
* connection put its connection name in its destination URL when it
* subscribed. An indication that has no matching connection is discarded.
*
* When the listener is paused, indications are queued.
*
* @param url The destination URL.
* @param indication The CIM indication.
* @param wasQueued true if this indication is from the queue, false
* otherwise.
*/
private void indicationOccured(String url, CIMInstance indication, boolean wasQueued) {
if (wasQueued) {
s_logger.debug("{} Dequeued: {}", new Object[] { url, indication.toString() });
} else {
s_logger.debug("{} Received: {}", new Object[] { url, indication.toString() });
}
Runnable indicationWorker = new IndicationWorkerThread(url, indication, wasQueued);
executorService.execute(indicationWorker);
}
/**
* Worker thread to spawn indications into the processors through thread pool.
*/
public class IndicationWorkerThread implements Runnable {
String url;
CIMInstance indication;
boolean wasQueued;
public IndicationWorkerThread(String url, CIMInstance indication, boolean wasQueued) {
this.url = url;
this.indication = indication;
this.wasQueued = wasQueued;
}
@Override
public void run() {
// Awful quick-fix to filter out a nuisance.
s_logger.debug("Inside IndicationWorkerThread");
try {
CimIndicationSet data = new CimIndicationSet(indication);
if ((data.isAlertIndication()) && (data.containsKey(CimConstants.PROBABLE_CAUSE_TAG_KEY))) {
String probableCause = data.get(CimConstants.PROBABLE_CAUSE_TAG_KEY);
if ((probableCause != null) && (probableCause.equals(CimConstants.STATISTICAL_DATA_UPDATE_SUCCESS))) {
s_logger.info("{} Discarded: Statistical Data Update.", url);
return;
}
}
} catch (Exception ex) {
s_logger.error("Error discarding statiustical data update", ex);
return;
}
// Queue the indication if listening is paused.
if (_isPaused) {
if (_queue.offer(new CimQueuedIndication(url, indication))) {
s_logger.debug("{} Queued: {}", new Object[] { url, indication.toString() });
} else {
s_logger.debug("Queue is full! {} Discarded: {}", new Object[] { url, indication.toString() });
}
return;
}
// The path SHOULD be a connection name.
//
// SBLIM CIM client version 2.1.7 only puts the path component
// in the URL. That "bug" is fixed in version 2.1.8. Check the
// URL with an inexpensive test until VOPS upgrades to using
// version 2.1.8.
//
// Does the URL appear to have a scheme?
String connectionName = url;
if (url.indexOf("://") != -1) {
try {
connectionName = new URL(url).getPath();
} catch (Exception e) {
s_logger.error(e.getMessage(), e);
}
}
// Look for a matching, registered connection. Reject
// the indication if there is no match.
//
// For some reason, SBLIM CIM client forces the path to
// lowercase. This "bug" has been reported, but has not
// been fixed as of version 2.1.8. To find a match, all
// names must be normalized to lowercase.
String key = connectionName.toLowerCase();
if (key.startsWith("/")) {
key = key.substring(1);
}
if (_connections.containsKey(key)) {
CimConnection connection = _connections.get(key);
publishIndication(indication, connection);
} else {
s_logger.debug("{} Rejected: {}", new Object[] { url, indication.toString() });
}
}
}
/**
* Forwards received CIM indications to a matching connection.
*
* @param url The destination URL.
* @param indication The CIM indication.
*/
public void indicationOccured(String url, CIMInstance indication) {
s_logger.debug("Indication occurred for {}", url);
indicationOccured(url, indication, false);
}
/**
* Pauses processing. While paused, received indications are queued.
*/
public synchronized void pause() {
if (!_isRunning || _isPaused) {
return;
}
s_logger.info("Pausing Listener.");
_isPaused = true;
}
/**
* Resumes processing. Queued indications are immediately processed.
*/
public synchronized void resume() {
if (!_isRunning || !_isPaused) {
return;
}
s_logger.info("Resuming Listener.");
_isPaused = false;
while (!_queue.isEmpty()) {
CimQueuedIndication element = _queue.remove();
String url = element.getURL();
CIMInstance indication = element.getIndication();
indicationOccured(url, indication, true);
}
}
/**
* Stops listening and releases the TCP port.
*/
public synchronized void stop() {
if (_isRunning) {
s_logger.info("Stopping listener at {}", _url);
_listener.removeListener(_url.getPort());
s_logger.info("Stopped listener at {}", _url);
_isRunning = false;
_listener = null;
}
}
/**
* close's exiting tcp secure port 7012 and re'opens new socket to indications from smi-s provider.
*
* @throws IOException
*/
public synchronized void restart() throws IOException {
s_logger.info("listener restart initiated");
stop();
startup();
}
/**
* Forwards the indication to the list of registered indication consumers.
* Note that the indication is first processed as specified by the consumer
* to transform the indication to the format expected by the consumer.
*/
private void publishIndication(CIMInstance indication, CimConnection connection) {
if (_indicationConsumers == null) {
s_logger.error("Indication consumers list is null.");
return;
}
// Loop over the consumers processing the indication as specified by
// the consumer and then forwarding the processed indication to the
// consumer.
for (CimIndicationConsumer consumer : _indicationConsumers) {
// Initialized the processed indication to the passed indication.
// If no processing is specified by the consumer, the raw indication
// is forwarded to the consumer.
Object processedIndication = indication;
// If the consumer specifies default processing should occur, this
// is done first.
CimIndicationProcessor processor = null;
if (consumer.getUseDefaultProcessor()) {
processor = connection.getDefaultIndicationProcessor();
processedIndication = processor.process(indication);
}
// Now if a custom processor is specified, the custom processor is
// called to do any further processing of the indication.
processor = consumer.getIndicationProcessor();
if (processor != null) {
processedIndication = processor.process(processedIndication);
}
// Now forward the processed indication to the consumer.
consumer.consumeIndication(processedIndication);
}
}
/**
*
* @param connectionInfo
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws IOException
* @throws KeyManagementException
* @throws ConnectionManagerException
*/
public void getClientCertificate(CimConnectionInfo connectionInfo)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException,
KeyManagementException, ConnectionManagerException {
char[] passphrase;
String passphraseStr = "changeit";
passphrase = passphraseStr.toCharArray();
KeyStore ks = getTrustStore(_trustStoreLocation, passphrase);
SSLContext context = SSLContext.getInstance("TLS");
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
X509TrustManager defaultTrustManager = (X509TrustManager) tmf
.getTrustManagers()[0];
TrustedCertManager tm = new TrustedCertManager(defaultTrustManager);
s_logger.debug("Created trust manager");
context.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory factory = context.getSocketFactory();
String smiHost = connectionInfo.getHost();
int smiPort = defaultSMISSSLPort;
if (connectionInfo.getUseSSL()) {
smiPort = connectionInfo.getPort();
}
s_logger.debug("Opening connection to {}:{}", smiHost, smiPort);
SSLSocket socket = (SSLSocket) factory.createSocket(smiHost, smiPort);
socket.setSoTimeout(10000);
try {
s_logger.debug("Starting SSL negotiation");
socket.startHandshake();
socket.close();
socket = null;
} catch (SSLException e) {
// We ignore this exception. What we really need is the SSL
// handshake results.
} finally {
if (socket != null) {
socket.close();
}
}
X509Certificate[] chain = tm.chain;
if (chain == null) {
s_logger.debug("Error getting client certificate chain");
throw new ConnectionManagerException(
"Error getting client certificate chain");
}
X509Certificate cert0 = chain[0];
String alias0 = smiHost + "-" + "1";
ks.setCertificateEntry(alias0, cert0);
s_logger.debug("Added a certificate to the truststore with alias: {}",
alias0);
File trustStoreOut = new File(_trustStoreLocation);
if (trustStoreOut.exists()) {
// Save the original truststore
File trustStoreOutSaved = new File(_trustStoreLocation + "~");
if (trustStoreOutSaved.exists()) {
trustStoreOut.delete();
}
trustStoreOut.renameTo(trustStoreOutSaved);
}
OutputStream out2 = new FileOutputStream(_trustStoreLocation);
ks.store(out2, passphrase);
out2.close();
s_logger.debug("Created/updated the trust store: {}",
_trustStoreLocation);
restart();
}
/**
* Gives trustStore
*
* @param trustStoreFileName
* @param passphrase
* @return
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws IOException
*/
private static KeyStore getTrustStore(String trustStoreFileName, char[] passphrase)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
InputStream in = null; // We will provide null when we need to create a new keystore
File file = new File(trustStoreFileName);
if (file.isFile() == true) {
in = new FileInputStream(file);
}
s_logger.debug("Loading the truststore: {}", file);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, passphrase);
if (in != null) {
in.close();
}
return ks;
}
private static class TrustedCertManager implements X509TrustManager {
private final X509TrustManager tm;
private X509Certificate[] chain;
TrustedCertManager(X509TrustManager tm) {
this.tm = tm;
}
public X509Certificate[] getAcceptedIssuers() {
throw new UnsupportedOperationException();
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
throw new UnsupportedOperationException();
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
if (chain != null) {
this.chain = Arrays.copyOf(chain, chain.length);
}
tm.checkServerTrusted(chain, authType);
}
}
}