/**
Copyright (C) 2012 Delcyon, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.delcyon.capo.client;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.SocketException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.TrustedCertificateEntry;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.logging.Level;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.xml.bind.DatatypeConverter;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.tanukisoftware.wrapper.WrapperManager;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.delcyon.capo.CapoApplication;
import com.delcyon.capo.Configuration;
import com.delcyon.capo.Configuration.PREFERENCE;
import com.delcyon.capo.controller.LocalRequestProcessor;
import com.delcyon.capo.controller.client.ControllerRequest;
import com.delcyon.capo.crypto.CertificateRequest;
import com.delcyon.capo.crypto.CertificateRequest.CertificateRequestType;
import com.delcyon.capo.datastream.StreamHandler;
import com.delcyon.capo.datastream.StreamProcessor;
import com.delcyon.capo.datastream.StreamUtil;
import com.delcyon.capo.preferences.Preference;
import com.delcyon.capo.preferences.PreferenceInfo;
import com.delcyon.capo.preferences.PreferenceInfoHelper;
import com.delcyon.capo.preferences.PreferenceProvider;
import com.delcyon.capo.protocol.client.CapoConnection;
import com.delcyon.capo.protocol.client.Request;
import com.delcyon.capo.resourcemanager.CapoDataManager;
import com.delcyon.capo.resourcemanager.ResourceDescriptor;
import com.delcyon.capo.resourcemanager.ResourceDescriptor.Action;
import com.delcyon.capo.resourcemanager.ResourceParameter;
import com.delcyon.capo.resourcemanager.remote.RemoteResourceResponseProcessor;
import com.delcyon.capo.resourcemanager.remote.RemoteResourceResponseProcessor.ThreadedInputStreamReader;
import com.delcyon.capo.resourcemanager.types.FileResourceType;
import com.delcyon.capo.tasks.TaskManagerThread;
import com.delcyon.capo.xml.XPath;
import com.delcyon.capo.xml.cdom.CNode;
/**
* @author jeremiah
*
*/
@PreferenceProvider(preferences=CapoClient.Preferences.class)
public class CapoClient extends CapoApplication
{
public enum Preferences implements Preference
{
@PreferenceInfo(arguments={"boolean"}, defaultValue="true", description="Run The Capo Client as a service [true|false] default is true", longOption="CLIENT_AS_SERVICE", option="CLIENT_AS_SERVICE")
CLIENT_AS_SERVICE,
@PreferenceInfo(arguments={"clientID"}, defaultValue="capo.client.0", description="ID that this server will use when communicating with servers", longOption="CLIENT_ID", option="CLIENT_ID")
CLIENT_ID,
@PreferenceInfo(arguments={"keysize"}, defaultValue="1024", description="Encryption key size", longOption="KEY_SIZE", option="KEY_SIZE")
KEY_SIZE,
@PreferenceInfo(arguments={"months"}, defaultValue="36", description="Number of Months before key expires", longOption="KEY_MONTHS_VALID", option="KEY_MONTHS_VALID")
KEY_MONTHS_VALID,
@PreferenceInfo(arguments={"interval"}, defaultValue="1000", description="Milliseconds until retry, on connection failure", longOption="CONNECTION_RETRY_INTERVAL", option="CONNECTION_RETRY_INTERVAL")
CONNECTION_RETRY_INTERVAL,
@PreferenceInfo(arguments={"boolean"}, defaultValue="false", description="This will cause the client to ignore any server requests to restart. This should only be used for testing. Defaults to false.", longOption="IGNORE_RESTART_REQUEST", option="IGNORE_RESTART_REQUEST",location=Location.CLIENT)
IGNORE_RESTART_REQUEST;
@Override
public String[] getArguments()
{
return PreferenceInfoHelper.getInfo(this).arguments();
}
@Override
public String getDefaultValue()
{
return java.util.prefs.Preferences.systemNodeForPackage(CapoApplication.getApplication().getClass()).get(getLongOption(), PreferenceInfoHelper.getInfo(this).defaultValue());
}
@Override
public String getDescription()
{
return PreferenceInfoHelper.getInfo(this).description();
}
@Override
public String getLongOption()
{
return PreferenceInfoHelper.getInfo(this).longOption();
}
@Override
public String getOption()
{
return PreferenceInfoHelper.getInfo(this).option();
}
@Override
public Location getLocation()
{
return PreferenceInfoHelper.getInfo(this).location();
}
}
private static final String APPLICATION_DIRECTORY_NAME = "client";
private static final long MAX_SHUTDOWN_WAIT_TIME = 10000; //10 seconds
private HashMap<String, String> idHashMap = new HashMap<String, String>();
public CapoClient() throws Exception
{
super();
}
@Override
public Integer start(String[] programArgs)
{
try
{
init(programArgs);
startup(programArgs);
} catch (Exception e)
{
e.printStackTrace();
return 1;
}
return null;
}
/**
* @param programArgs
*/
public static void main(String[] programArgs)
{
try
{
WrapperManager.start( new CapoClient(), programArgs );
}
catch (Exception e)
{
e.printStackTrace();
System.exit(1);
}
}
protected void init(String[] programArgs) throws Exception
{
setApplicationState(ApplicationState.INITIALIZING);
setConfiguration(new Configuration(programArgs));
if (getConfiguration().hasOption(PREFERENCE.HELP))
{
getConfiguration().printHelp();
System.exit(0);
}
//System.setProperty("javax.net.ssl.keyStore", getConfiguration().getValue(PREFERENCE.KEYSTORE));
System.setProperty("javax.net.ssl.keyStorePassword", getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD));
setDataManager(CapoDataManager.loadDataManager(getConfiguration().getValue(PREFERENCE.RESOURCE_MANAGER)));
getDataManager().init();
runStartupScript(getConfiguration().getValue(PREFERENCE.STARTUP_SCRIPT));
setApplicationState(ApplicationState.INITIALIZED);
}
private void runStartupScript(String startupScriptName) throws Exception
{
ResourceDescriptor startupScriptFile = getDataManager().getResourceDescriptor(null,startupScriptName);
startupScriptFile.addResourceParameters(null,new ResourceParameter(FileResourceType.Parameters.PARENT_PROVIDED_DIRECTORY,PREFERENCE.CONFIG_DIR));
if (startupScriptFile.getResourceMetaData(null).exists() == false)
{
startupScriptFile.performAction(null, Action.CREATE);
startupScriptFile.close(null);
startupScriptFile.open(null);
Document startupDocument = CapoApplication.getDefaultDocument("client_startup.xml");
OutputStream startupFileOutputStream = startupScriptFile.getOutputStream(null);
TransformerFactory tFactory = TransformerFactory.newInstance();
Transformer transformer = tFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(new DOMSource(startupDocument), new StreamResult(startupFileOutputStream));
startupFileOutputStream.close();
}
LocalRequestProcessor localRequestProcessor = new LocalRequestProcessor();
localRequestProcessor.process(CapoApplication.getDocumentBuilder().parse(startupScriptFile.getInputStream(null)));
startupScriptFile.close(null);
}
@Override
protected void startup(String[] programArgs) throws Exception
{
start();
//keep this thread running until the client thread is ready.
while(getApplicationState().ordinal() < ApplicationState.INITIALIZED.ordinal())
{
Thread.sleep(500);
}
}
@Override
public void run()
{
try
{
HashMap<String, String> sessionHashMap = new HashMap<String, String>();
//get list of RequestProducers
//get list of ServerResponseConsumers
//get ordered list of initial requests
CapoConnection capoConnection = new CapoConnection();
runUpdateRequest(capoConnection,sessionHashMap);
capoConnection.close();
if (WrapperManager.isShuttingDown()) //bail out if we're restarting
{
setApplicationState(ApplicationState.STOPPING);
return;
}
if (hasValidKeystore() == false)
{
//setup keystore
capoConnection = new CapoConnection();
setupKeystore(capoConnection);
capoConnection.close();
}
else
{
loadKeystore();
}
setupSSL();
//verify identity scripts
//run identity scripts
capoConnection = new CapoConnection();
runIdentityRequest(capoConnection,sessionHashMap);
// capoConnection.close();
//
// capoConnection = new CapoConnection();
runTasksUpdateRequest(capoConnection,sessionHashMap);
// capoConnection.close();
//
// capoConnection = new CapoConnection();
runDefaultRequest(capoConnection,sessionHashMap);
capoConnection.close();
TaskManagerThread.startTaskManagerThread();
} catch (Exception e)
{
//if something else is monitoring this client, don't exit on error, leave it to the monitor to do so.
if (getExceptionList() != null)
{
getExceptionList().add(e);
}
else //we're on our own here, so just exit.
{
CapoApplication.logger.log(Level.SEVERE, "Exception thrown in main processing loop. Restarting.",e);
WrapperManager.restart();
}
}
setApplicationState(ApplicationState.READY);
}
public void runDefaultRequest(CapoConnection capoConnection, HashMap<String, String> sessionHashMap) throws Exception
{
ControllerRequest controllerRequest = new ControllerRequest(capoConnection.getOutputStream(),capoConnection.getInputStream());
//load client variables
controllerRequest.loadSystemVariables();
runRequest(capoConnection, controllerRequest,sessionHashMap);
}
public void runIdentityRequest(CapoConnection capoConnection, HashMap<String, String> sessionHashMap) throws Exception
{
ControllerRequest controllerRequest = new ControllerRequest(capoConnection.getOutputStream(),capoConnection.getInputStream());
controllerRequest.setType("identity");
controllerRequest.loadSystemVariables();
runRequest(capoConnection, controllerRequest,sessionHashMap);
}
public void runUpdateRequest(CapoConnection capoConnection,HashMap<String, String> sessionHashMap) throws Exception
{
ControllerRequest controllerRequest = new ControllerRequest(capoConnection.getOutputStream(),capoConnection.getInputStream());
controllerRequest.setType("update");
controllerRequest.loadSystemVariables();
runRequest(capoConnection, controllerRequest,sessionHashMap);
}
public void runTasksUpdateRequest(CapoConnection capoConnection,HashMap<String, String> sessionHashMap) throws Exception
{
ControllerRequest controllerRequest = new ControllerRequest(capoConnection.getOutputStream(),capoConnection.getInputStream());
controllerRequest.setType("tasks_update");
controllerRequest.loadSystemVariables();
runRequest(capoConnection, controllerRequest,sessionHashMap);
}
public void runRequest(CapoConnection capoConnection, Request request, HashMap<String, String> sessionHashMap) throws Exception
{
String initialRequestType = null;
if (request instanceof ControllerRequest)
{
initialRequestType = ((ControllerRequest) request).getType();
if (initialRequestType == null || initialRequestType.isEmpty())
{
initialRequestType = "default";
}
}
else
{
initialRequestType = request.getClass().getSimpleName();
}
//send request
try
{
CapoApplication.logger.log(Level.INFO, "STARTING "+initialRequestType+" request.");
request.send();
}
catch (SocketException socketException)
{
socketException.printStackTrace();
//do nothing, let any errors be processed later, since there might be a message in the buffer
}
boolean isFinished = false;
int emptyCount = 0;
while(isFinished == false && emptyCount < 20)
{
byte[] buffer = getBuffer(capoConnection.getInputStream());
if(buffer == null)
{
//connection closed
CapoApplication.logger.log(Level.WARNING, "Server Connection closed unexpectedly for "+initialRequestType+" request.");
break;
}
//figure out the kind of response
StreamProcessor streamProcessor = StreamHandler.getStreamProcessor(buffer);
if (streamProcessor != null)
{
streamProcessor.init(sessionHashMap);
streamProcessor.processStream(capoConnection.getInputStream(), capoConnection.getOutputStream());
}
else
{
//if we have no data, then we are finished, otherwise wait, then try again?
if (buffer.length == 0)
{
CapoApplication.logger.log(Level.WARNING, "Empty Response from server for "+initialRequestType+" request. Going to wait for more data.");
emptyCount++;
}
else
{
String bufferString = new String(buffer);
if(bufferString.startsWith("FINISHED:"))
{
int count = 0;
while(RemoteResourceResponseProcessor.getThreadedInputStreamReaderHashtable().size() != 0)
{
CapoApplication.logger.log(Level.WARNING, "We shouldn't be done yet!");
Thread.sleep(1000);
count++;
if(count >= 30)
{
CapoApplication.logger.log(Level.SEVERE, "Well, that didn't work, killing all of the connections.");
Enumeration<ThreadedInputStreamReader> threadedInputStreamReaderEnumeration = RemoteResourceResponseProcessor.getThreadedInputStreamReaderHashtable().elements();
while(threadedInputStreamReaderEnumeration.hasMoreElements())
{
ThreadedInputStreamReader threadedInputStreamReader = threadedInputStreamReaderEnumeration.nextElement();
threadedInputStreamReader.close();
}
RemoteResourceResponseProcessor.getThreadedInputStreamReaderHashtable().clear();
break;
}
}
StreamUtil.fullyReadUntilPattern(capoConnection.getInputStream(), false, (byte)0);
CapoApplication.logger.log(Level.INFO, "FINISHED "+initialRequestType+" request.");
isFinished = true;
}
else
{
CapoApplication.logger.log(Level.WARNING, "Don't know what to do with '"+bufferString+"', finishing "+initialRequestType+" request.");
isFinished = true;
}
}
}
}
}
private byte[] getBuffer(BufferedInputStream inputStream) throws Exception
{
int bufferSize = getConfiguration().getIntValue(PREFERENCE.BUFFER_SIZE);
byte[] buffer = new byte[bufferSize];
inputStream.mark(bufferSize);
int bytesRead = StreamUtil.fullyReadIntoBufferUntilPattern(inputStream, buffer, (byte)0);
inputStream.reset();
//truncate the buffer so we can do accurate length checks on it
//totally pointless, but seems like a good idea at the time
if (bytesRead < 0)
{
return null;
}
else if (bytesRead < bufferSize)
{
byte[] shortenedBuffer = new byte[bytesRead];
System.arraycopy(buffer, 0, shortenedBuffer, 0, bytesRead);
return shortenedBuffer;
}
else
{
return buffer;
}
}
@Override
public String getApplicationDirectoryName()
{
return APPLICATION_DIRECTORY_NAME;
}
public void clearIDMap()
{
idHashMap.clear();
}
public void setID(String name, String value)
{
idHashMap.put(name, value);
}
public HashMap<String, String> getIDMap()
{
return idHashMap;
}
private boolean hasValidKeystore() throws Exception
{
boolean keyStoreIsValid = true;
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
ResourceDescriptor keystoreFile = getDataManager().getResourceDescriptor(null,getConfiguration().getValue(PREFERENCE.KEYSTORE));
keystoreFile.addResourceParameters(null,new ResourceParameter(FileResourceType.Parameters.PARENT_PROVIDED_DIRECTORY,PREFERENCE.CONFIG_DIR));
if (keystoreFile.getResourceMetaData(null).exists() == false)
{
return false;
}
char[] password = getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD).toCharArray();
InputStream keyStoreFileInputStream = keystoreFile.getInputStream(null);
keyStore.load(keyStoreFileInputStream, password);
keyStoreFileInputStream.close();
String clientID = getConfiguration().getValue(CapoClient.Preferences.CLIENT_ID);
if (keyStore.containsAlias(clientID+".private") == false)
{
return false;
}
return keyStoreIsValid;
}
private void loadKeystore() throws Exception
{
// load the file
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
ResourceDescriptor keystoreResourceDescriptor = getDataManager().getResourceDescriptor(null, getConfiguration().getValue(PREFERENCE.KEYSTORE));
keystoreResourceDescriptor.addResourceParameters(null,new ResourceParameter(FileResourceType.Parameters.PARENT_PROVIDED_DIRECTORY,PREFERENCE.CONFIG_DIR));
char[] password = getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD).toCharArray();
InputStream keyStoreFileInputStream = keystoreResourceDescriptor.getInputStream(null);
keyStore.load(keyStoreFileInputStream, password);
keyStoreFileInputStream.close();
setKeyStore(keyStore);
//System.setProperty("javax.net.ssl.keyStore", keystoreFile.getCanonicalPath());
System.setProperty("javax.net.ssl.keyStorePassword", getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD));
}
private void setupSSL() throws Exception
{
// set the ssl context to load using our newly created trustmanager which has our keys in it.
SSLContext sslContext = SSLContext.getInstance("SSL");
// initialize a key manager factory with our keystore
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(getKeyStore(), getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD).toCharArray());
// initialize a trust manager factory with our keystore
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(getKeyStore());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new java.security.SecureRandom());
setSslSocketFactory(sslContext.getSocketFactory());
}
private void setupKeystore(CapoConnection capoConnection) throws Exception
{
// load the file
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
ResourceDescriptor keystoreFile = getDataManager().getResourceDescriptor(null, getConfiguration().getValue(PREFERENCE.KEYSTORE));
keystoreFile.addResourceParameters(null,new ResourceParameter(FileResourceType.Parameters.PARENT_PROVIDED_DIRECTORY,PREFERENCE.CONFIG_DIR));
char[] password = getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD).toCharArray();
if (keystoreFile.getResourceMetaData(null).exists() == false)
{
KeyPairGenerator rsakeyPairGenerator = KeyPairGenerator.getInstance("RSA");
rsakeyPairGenerator.initialize(getConfiguration().getIntValue(Preferences.KEY_SIZE));
KeyPair rsaKeyPair = rsakeyPairGenerator.generateKeyPair();
//get certificate from server
CertificateRequest certificateRequest = new CertificateRequest(capoConnection);
certificateRequest.setCertificateRequestType(CertificateRequestType.DH);
certificateRequest.loadDHPhase1();
certificateRequest.init();
certificateRequest.send();
certificateRequest.parseResponse();
//this is where the server assigns our client ID
String clientID = certificateRequest.getParameter(CertificateRequest.Attributes.CLIENT_ID);
getConfiguration().setValue(Preferences.CLIENT_ID,clientID);
byte[] certificateEncoding = certificateRequest.getDecryptedPayload();
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certificateEncoding));
String serverPassword = CapoApplication.getConfiguration().getValue(PREFERENCE.CLIENT_VERIFICATION_PASSWORD);
if (serverPassword.isEmpty())
{
System.out.println("Enter Password:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
serverPassword = br.readLine();
}
certificateRequest.setPayload(serverPassword);
certificateRequest.setParameter(CertificateRequest.Attributes.CLIENT_PUBLIC_KEY, DatatypeConverter.printBase64Binary(rsaKeyPair.getPublic().getEncoded()));
((CNode) certificateRequest.getRequestDocument().getDocumentElement()).setNodeName("ClientResponse");
certificateRequest.resend();
//we need to make sure that we wait for a server response here, so that we don't continue processing until the server has had time to update its keystore.
Element responseElement = certificateRequest.readResponse().getDocumentElement();
if (responseElement.hasAttribute("result") == false)
{
throw new Exception("Server did NOT process key request, check logs.");
}
else if (responseElement.getAttribute("result").equals("WRONG_PASSWORD"))
{
throw new Exception("Wrong Password.");
}
else if (responseElement.getAttribute("result").equals("SUCCESS") == false)
{
XPath.dumpNode(responseElement, System.err);
throw new Exception("Server did NOT process key request, check logs.");
}
keyStore.load(null, password);
KeyStore.TrustedCertificateEntry trustedCertificateEntry = new TrustedCertificateEntry(certificate);
keyStore.setEntry(certificateRequest.getParameter(CertificateRequest.Attributes.SERVER_ID), trustedCertificateEntry,null);
KeyStore.PrivateKeyEntry privateKeyEntry = new PrivateKeyEntry(rsaKeyPair.getPrivate(), new Certificate[]{certificate});
keyStore.setEntry(getConfiguration().getValue(Preferences.CLIENT_ID)+".private", privateKeyEntry,new KeyStore.PasswordProtection(password));
if (keystoreFile.getResourceMetaData(null).exists() == false)
{
keystoreFile.performAction(null, Action.CREATE);
keystoreFile.close(null);
keystoreFile.open(null);
}
OutputStream keyStoreFileOutputStream = keystoreFile.getOutputStream(null);
keyStore.store(keyStoreFileOutputStream, password);
keyStoreFileOutputStream.close();
setKeyStore(keyStore);
}
else
{
loadKeystore();
}
}
public void shutdown() throws Exception
{
CapoApplication.logger.log(Level.INFO,"Waiting for processing to finish.");
while(getApplicationState().ordinal() < ApplicationState.READY.ordinal())
{
Thread.sleep(500);
}
if (TaskManagerThread.getTaskManagerThread() != null)
{
CapoApplication.logger.log(Level.INFO,"Stopping Task Manager");
TaskManagerThread.getTaskManagerThread().interrupt();
long totalWaitTime = 0;
long waitTime = 500;
while(TaskManagerThread.getTaskManagerThread().getTaskManagerState() != ApplicationState.STOPPED || totalWaitTime >= MAX_SHUTDOWN_WAIT_TIME)
{
try
{
Thread.sleep(waitTime);
totalWaitTime += waitTime;
}
catch (InterruptedException interruptedException)
{
CapoApplication.logger.log(Level.WARNING,"Ignoring InterruptedException");
}
}
CapoApplication.logger.log(Level.INFO,"Done Stopping Task Manager");
}
CapoApplication.logger.log(Level.INFO,"Releaseing Data Manager");
getDataManager().release();
setDataManager(null);
CapoApplication.logger.log(Level.INFO,"Done.");
}
}