/**
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.protocol.client;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.logging.Level;
import javax.net.ssl.SSLSocket;
import javax.xml.bind.DatatypeConverter;
import com.delcyon.capo.CapoApplication;
import com.delcyon.capo.CapoApplication.ApplicationState;
import com.delcyon.capo.Configuration.PREFERENCE;
import com.delcyon.capo.client.CapoClient;
import com.delcyon.capo.datastream.StreamEventFilterInputStream;
import com.delcyon.capo.datastream.StreamEventListener;
import com.delcyon.capo.datastream.StreamUtil;
/**
* @author jeremiah
*
*/
public class CapoConnection implements StreamEventListener
{
public enum ConnectionTypes
{
CAPO_REQUEST
}
public enum ConnectionResponses
{
OK,
BUSY
}
private Socket socket;
private BufferedInputStream inputStream;
private OutputStream outputStream;
private String serverAddress;
private int port;
private boolean dumpOnClose = false;
private StackTraceElement[] callerStackTraceElements;
@SuppressWarnings("unused")
private StackTraceElement[] closerStackTraceElements;
@SuppressWarnings("unused")
private StackTraceElement[] inputStreamcallerStackTraceElements;
@SuppressWarnings("unused")
private StackTraceElement[] inputStreamcloserStackTraceElements;
private String sslSID;
public CapoConnection() throws Exception
{
serverAddress = CapoApplication.getConfiguration().getValue(PREFERENCE.SERVER_LIST).split(",")[0];
port = CapoApplication.getConfiguration().getIntValue(PREFERENCE.PORT);
open();
}
// public CapoConnection(String serverAddress,int port) throws Exception
// {
// this.serverAddress = serverAddress;
// this.port = port;
// open();
// }
/**
* A little control logic so we can reuse sockets if we have them, as well as figuring out which server to connect to
* @param socket
* @return
* @throws Exception
*/
public Socket open() throws Exception
{
callerStackTraceElements = new Exception().getStackTrace();
while(true)
{
while (this.socket == null || socket.isClosed())
{
try
{
if (CapoApplication.getSslSocketFactory() != null)
{
CapoClient.logger.log(Level.FINE, "Opening Secure Socket to "+serverAddress+":"+port);
this.socket = CapoApplication.getSslSocketFactory().createSocket(serverAddress, port);
this.socket.setSendBufferSize(CapoApplication.getConfiguration().getIntValue(PREFERENCE.BUFFER_SIZE)+728);
this.socket.setReceiveBufferSize(CapoApplication.getConfiguration().getIntValue(PREFERENCE.BUFFER_SIZE)+728);
}
else
{
CapoClient.logger.log(Level.FINE, "Opening Socket to "+serverAddress+":"+port);
this.socket = new Socket(serverAddress, port);
}
}
catch (ConnectException connectException)
{
if(CapoApplication.getApplication().getApplicationState().ordinal() >= ApplicationState.STOPPING.ordinal())
{
throw new Exception("Application Shutting Down, ABORTING connection attempt.");
}
CapoApplication.logger.log(Level.WARNING, "Error opening socket, sleeping "+CapoApplication.getConfiguration().getLongValue(CapoClient.Preferences.CONNECTION_RETRY_INTERVAL)+"ms");
Thread.sleep(CapoApplication.getConfiguration().getLongValue(CapoClient.Preferences.CONNECTION_RETRY_INTERVAL));
}
}
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
socket.setSoLinger(false, 0);
this.outputStream = socket.getOutputStream();
//This is just for debugging when needed.
InputStream tempInputStream = socket.getInputStream();
if(CapoApplication.logger.isLoggable(Level.FINE))
{
inputStreamcallerStackTraceElements = new Exception().getStackTrace();
tempInputStream = new StreamEventFilterInputStream(tempInputStream);
((StreamEventFilterInputStream) tempInputStream).addStreamEventListener(this);
}
this.inputStream = new BufferedInputStream(tempInputStream);
if (CapoApplication.getKeyStore() != null && CapoApplication.getSslSocketFactory() != null)
{
sslSID = DatatypeConverter.printHexBinary(((SSLSocket)socket).getSession().getId());
CapoApplication.logger.fine("SSL SID:"+sslSID);
String clientID = CapoApplication.getConfiguration().getValue(CapoClient.Preferences.CLIENT_ID);
char[] password = CapoApplication.getConfiguration().getValue(PREFERENCE.KEYSTORE_PASSWORD).toCharArray();
String authMessage = "AUTH:CID="+clientID;
PrivateKey privateKey = (PrivateKey) CapoApplication.getKeyStore().getKey(clientID+".private", password);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(clientID.getBytes());
signature.update(((SSLSocket)socket).getSession().getId());
authMessage += ":SIG="+DatatypeConverter.printHexBinary(signature.sign())+":";
this.outputStream.write(authMessage.getBytes());
this.outputStream.flush();
this.inputStream.read();
}
//check for a busy signal
this.outputStream.write(ConnectionTypes.CAPO_REQUEST.toString().getBytes());
this.outputStream.flush();
byte[] buffer = new byte[256];
StreamUtil.fullyReadIntoBufferUntilPattern(inputStream, buffer, (byte)0);
String message = new String(buffer).trim();
//check to see if this is a busy message
if (message.matches(ConnectionResponses.BUSY+" \\d+"))
{
long delaytime = Long.parseLong(message.replaceAll(ConnectionResponses.BUSY+" (\\d+)", "$1"));
close();
CapoApplication.logger.log(Level.WARNING, "Server Busy. Retrying connection in "+delaytime+"ms.");
Thread.sleep(delaytime);
continue;
}
else if (message.matches(ConnectionResponses.OK.toString()) == false)
{
throw new Exception("Unknown message from server: '"+message+"'");
}
break;
}
CapoApplication.logger.log(Level.INFO, "Opened Socket: "+socket);
return socket;
}
//CS & SS
@Override
protected void finalize() throws Throwable
{
super.finalize();
close();
}
//CS & SS
public void close()
{
if (socket != null)
{
try
{
closerStackTraceElements = new Exception("Closing stack trace").getStackTrace();
if(dumpOnClose && socket.isClosed() == false && CapoApplication.logger.isLoggable(Level.FINE))
{
System.err.println("Closing stack trace");
Thread.dumpStack();
Exception exception = new Exception("Opening stack trace");
exception.setStackTrace(callerStackTraceElements);
exception.printStackTrace();
}
CapoApplication.logger.log(Level.INFO, "Closing Socket: "+socket);
socket.close();
// if (socket instanceof SSLSocket)
// {
// ((SSLSocket) socket).getSession().invalidate();
// }
socket = null;
}
catch (Exception e)
{
//we don't really care if this fails.
}
}
if (outputStream != null)
{
try
{
outputStream.close();
}
catch (Exception e)
{
//do nothing on failure
}
}
if (inputStream != null)
{
try
{
inputStream.close();
}
catch (Exception e)
{
//do nothing on failure
}
}
}
/**
* This returns the output stream of the connection
* @return
*/
public OutputStream getOutputStream()
{
return this.outputStream;
}
public BufferedInputStream getInputStream()
{
return this.inputStream;
}
public void dumpOnClose(boolean dumpOnClose)
{
System.out.println("===================SETTING DUMP ON CLOSE====================");
this.dumpOnClose = true;
}
@Override
public void processStreamEvent(StreamEvent streamEvent) throws IOException
{
if(streamEvent == StreamEvent.CLOSED && dumpOnClose)
{
inputStreamcloserStackTraceElements = new Exception().getStackTrace();
System.err.println("Closing stack trace");
Thread.dumpStack();
Exception exception = new Exception("Opening stack trace");
exception.setStackTrace(callerStackTraceElements);
exception.printStackTrace();
}
}
}