package uc.protocols.client;
import uc.ConnectionHandler;
import uc.DCClient;
import uc.IHasUser;
import uc.IUser;
import uc.crypto.HashValue;
import uc.files.IDownloadable;
import uc.files.IHasDownloadable;
import uc.files.downloadqueue.AbstractDownloadQueueEntry;
import uc.files.transfer.AbstractFileTransfer;
import uc.files.transfer.FileTransferInformation;
import uc.files.transfer.IFileTransfer;
import uc.files.transfer.Slot;
import uc.protocols.ADCStatusMessage;
import uc.protocols.CPType;
import uc.protocols.ConnectionState;
import uc.protocols.DCProtocol;
import uc.protocols.IConnection;
import uc.protocols.TransferType;
import uc.protocols.UnblockingConnection;
import uc.protocols.hub.Flag;
import helpers.GH;
import helpers.SchedulableTask;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.nio.channels.ByteChannel;
import java.nio.channels.SocketChannel;
import logger.LoggerFactory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.Platform;
/**
*
* @author Quicksilver A class standing for a client to client
* connectionProtocol in NMDC
*
*
*/
public class ClientProtocol extends DCProtocol implements IHasUser, IHasDownloadable {
private static final Logger logger = LoggerFactory.make();
private final SchedulableTask loginTimeOut;
private final SchedulableTask transferCreateTimeOut;
// private volatile int timerLogintimeout = 0;
// private volatile int timerTransferCreateTimeout = 0;
// private volatile int awaitingADCGet = 0;
// private volatile boolean getAwaited = false;
private final ConnectionHandler ch;
private final boolean incoming;
private volatile String disconnectReason = null;
/**
* if CPSM is applicable for this connection here it is stored
* from identifying other user to close of connection
*/
private ClientProtocolStateMachine cpsm;
private IUser self;
private final List<String> debugMessages = Collections.synchronizedList(new ArrayList<String>());
/**
* if we should immediately reconnect true if
* for example a download was successful
*/
private boolean immediateReconnect = false;
private volatile Slot slot;
private final int myNumber = GH.nextInt(0x7FFF);
private int othersNumber = -1; // refers to the number of the other user in
// the competition who may download...
private volatile InetAddress otherip;
private Set<String> othersSupports = new HashSet<String>(); // The SupportString of the other side
/**
* information that is retrieved will be set to this ..
*/
private final FileTransferInformation fti;
/**
* if a FileTransfer is running.. then it is referenced here..
*/
private volatile AbstractFileTransfer fileTransfer = null;
//counter on what was last requested: if same file gets requested to often -> say we don't have
private long lastRequestpos = -1;
private int lastRequestCounter= 0;
//NMDC data
private static final int loginTargetLevel = 6;
/**
* com variable .. to make sure everything required was received
* things needed for Login: received: Sent/Received Direction + Sent/Received Supports + Sent/Received Key
* nr of things received if this reaches 6 login has finished
*/
private int loginLevel = 0;
// ADC data
/**
* the token responsible for this connection
*/
private volatile String token;
/**
* hubaddress told to us via REF in NMDC ...
* -> used to help finding out who connected to us.
*/
private volatile String hubaddy;
/**
* incoming .. constructor
*/
public ClientProtocol(SocketChannel sc, ConnectionHandler ch,boolean encryption) {
this((Object)sc,true,ch,null,null,null,null,encryption);
}
/**
* constructor for outgoing connections
* if we connect ourself we know in which hub we got the CTM and therefore
* know the self
*
* @param addy -> target
* @param ch
* @param self
* @param protocol
* @param token
* @param encryption
*/
public ClientProtocol(InetSocketAddress addy, ConnectionHandler ch,IUser self,IUser other,
CPType protocol,String token,boolean encryption) {
this((Object)addy,false,ch,self,other,protocol,token,encryption);
}
private ClientProtocol(Object addyOrSC ,boolean incoming, ConnectionHandler ch,IUser self,IUser other,
CPType protocol,String token,boolean encryption) {
super(new int[]{0,0,1}); //we want bandwidth mostly C-C protocol..
this.ch = ch;
DCClient dcc = ch.getDCC();
fti = new FileTransferInformation(dcc);
this.incoming = incoming;
this.self = self; // our self the user we represent in that hub may be null
othersNumber = 0; // others number -> NMDC data..
immediateReconnect = false;
setProtocolNMDC(protocol == null? null : protocol.isNmdc());
this.token = token;
Assert.isTrue(incoming || self != null );
Assert.isTrue(incoming || protocol.isNmdc() || token != null , " token should not be null for ADC connections");
HashValue fingerPrint = other != null? other.getKeyPrint(): null;
if (addyOrSC instanceof SocketChannel) {
connection = new UnblockingConnection(ch.getIdentity().getCryptoManager(), (SocketChannel)addyOrSC, this,encryption,incoming,fingerPrint);
} else {
connection = new UnblockingConnection(ch.getIdentity().getCryptoManager(),(InetSocketAddress)addyOrSC, this,encryption,fingerPrint);
}
loginTimeOut = new SchedulableTask(new Runnable() {
@Override
public void run() {
logger.debug("disconnect sent from here");
disconnect(DisconnectReason.CONNECTIONTIMEOUT);
}
},dcc.getSchedulerDir());
transferCreateTimeOut = new SchedulableTask(new Runnable() {
@Override
public void run() {
logger.debug("disconnect sent from here2");
disconnect(DisconnectReason.CONNECTIONTIMEOUT);
}
},dcc.getSchedulerDir());
}
public DCClient getDcc() {
return ch.getDCC();
}
public void start() {
WriteLock l = ClientProtocol.this.writeLock();
l.lock();
try {
connection.start();
super.start();
loginTimeOut.reschedule(60, TimeUnit.SECONDS);
} finally {
l.unlock();
}
}
public void beforeConnect() {
logger.debug("called beforeConnect");
super.beforeConnect();
disconnectReason = null;
}
public void onConnect() throws IOException {
logger.debug("called OnConnect");
InetSocketAddress isa = connection.getInetSocketAddress();
if (isa == null) {
throw new IOException("SocketAddress not set");
}
super.onConnect();
otherip = isa.getAddress();
Assert.isNotNull(otherip);
if (!isServer()) {
if (nmdc) { //outgoing therefore the protocol is already set..
MyNick.sendMyNickAndLock(this);
} else {
SUP.sendSUP(this);
INF.sendINFClientToServer(this);
}
}
//if not server nmdc variable is not set!
}
@Override
public void receivedCommand(String command) throws IOException,ProtocolException {
logger.debug("received command: "+command);
debugMessages.add("in: "+command);
super.receivedCommand(command);
}
/**
* OnLogin() called when the other is identified and we are ready to send an GET
*
* from here on is guaranteed the other User is set.. also direction is already
* determined
*
* if we are downloading here is the place to send ADCGET else we will do
* nothing and just wait for the other to send something
*/
public void onLogIn() throws IOException {
logger.debug("called OnLogIn ");
super.onLogIn();
loginTimeOut.cancelScheduled();
// logger.info("scheduling transferCreateTimeOut: li "+toString());
transferCreateTimeOut.reschedule(30, TimeUnit.SECONDS);
if (fti.isDownload()) { // Here call the connection handler to know what
// we want... if we want something..
// register state machine.. so it gets notified when this connection closes
cpsm = ch.getStateMachine(fti.getOther());
if (cpsm == null) {
disconnect(DisconnectReason.UNKNOWN);
return;
}
// super.onLogIn();
//fill in all FTI information
AbstractDownloadQueueEntry adqe = fti.getDqe();
if (adqe == null) {
throw new IOException("Protocol in invalid state");
}
fti.setType(adqe.getType());
if (adqe.getType() == TransferType.FILE) {
fti.setNameOfTransferred(fti.getDqe().getFileName());
}
boolean otherSupportsCompression = othersSupports.contains("ZLIG");
fti.setCompression( connection.isLocal(), otherSupportsCompression );
WriteLock l = ClientProtocol.this.writeLock();
l.lock();
try {
ch.notifyOfChange(ConnectionHandler.USER_IDENTIFIED_IN_CONNECTION, this, cpsm); //new ChangeNotification
} finally {
l.unlock();
}
logger.debug("trying to get a downlaod from dqe");
if (adqe.getDownload(fti)) { //fill in interval and HashValue..
logger.debug("got download sending ADCGET");
try {
if (nmdc) {
ADCGET.sendADCGET(this);
} else {
GET.sendGET(this);
}
} catch (IOException ioe) {
logger.debug(ioe);
disconnect(ioe.getMessage() != null? ioe.getMessage():DisconnectReason.UNKNOWN.toString());
}
} else {
logger.debug("disconnecting: IllegalState in DQE");
//what do now? nothing filled in.. may be just disconnect..
//probably reason for this is that the file was just finished..
immediateReconnect = true;
disconnect(DisconnectReason.ILLEGALSTATEERROR);
}
} else {
// super.onLogIn();
cpsm = null;
WriteLock l = ClientProtocol.this.writeLock();
l.lock();
try {
ch.notifyOfChange(ConnectionHandler.USER_IDENTIFIED_IN_CONNECTION, this, null);
} finally {
l.unlock();
}
}
}
/**
* called to simulate an anew
* login used to have multiple transfers on one connection
*/
private void relogin() {
logger.debug("relogin");
ch.getDCC().executeDir(new Runnable() {
public void run() {
WriteLock l = ClientProtocol.this.writeLock();
l.lock();
try {
if (fti.isDownload()) {
logger.debug("Relogin Download: "+fti.getOther().getNick());
AbstractDownloadQueueEntry dqe = fti.getOther()
.resolveDQEToUser();
if (dqe != null) {
fti.setDqe(dqe);
fti.setType(fti.getDqe().getType());
if (fti.getDqe().getType() == TransferType.FILE) {
fti.setNameOfTransferred(fti.getDqe().getFileName());
}
if (fti.getDqe().getDownload(fti)) { //fill in interval and HashValue..
logger.debug("got download sending ADCGET");
try {
if (nmdc) {
ADCGET.sendADCGET(ClientProtocol.this);
} else {
GET.sendGET(ClientProtocol.this);
}
} catch (IOException ioe) {
disconnect(DisconnectReason.CONNECTIONTIMEOUT);
}
}
} else {
connection.close();
}
}
} finally {
l.unlock();
}
}
});
}
public void onDisconnect() throws IOException {
logger.debug("called OnDisconnect "+fti.getOther());
super.onDisconnect();
ch.notifyOfChange(ConnectionHandler.CONNECTION_CLOSED,this,cpsm);
if (fti.getOther() != null) {
fti.getOther().deleteConnection(this);
}
loginTimeOut.cancelScheduled();
// logger.info("Canceling login timeout: dc "+toString());
transferCreateTimeOut.cancelScheduled();
// logger.info("Canceling transferCreateTimeOut: dc "+toString());
//ch.removeCons(this);
end(); //never reuse..
if (slot != null) {
ch.getSlotManager().returnSlot(slot,fti.getOther());
if (Platform.inDevelopmentMode()) {
logger.warn("slot returned too late: "+getUser());
}
}
}
/**
* called when other client tells that
* there are no free slots available.
*
* @param additionalMessage should be empty if none..
*/
void noSlotsAvailable(String additionalMessage) {
if (GH.isNullOrEmpty(additionalMessage)) {
disconnect(DisconnectReason.NOSLOTS);
} else {
disconnect(DisconnectReason.NOSLOTS+" "+additionalMessage);
}
}
@Override
protected void onUnexpectedCommandReceived(String command) {
//super.onUnexpectedCommandReceived(command);
logger.debug("Unexpected comamnd: "+command);
}
protected void onMalformedCommandReceived(String command) {
logger.debug("Malformed comamnd: "+command+" "+ getUser() != null? getUser():"");
}
/**
* @param otherwantsToDownload if the other requests to download -(does not matter for ADC)
*/
void setDownload(boolean otherwantsToDownload) throws IOException {
if (othersNumber == -1) {
throw new IllegalStateException("this may not be called before others number is set");
}
if (nmdc) {
if (otherwantsToDownload) {
fti.setDownload(fti.getDqe() != null && myNumber > othersNumber);
} else {
fti.setDownload(fti.getDqe() != null);
}
} else {
ClientProtocolStateMachine cpsm = getCh().getStateMachine(fti.getOther());
fti.setDownload(cpsm != null && cpsm.getToken().equals(token));
addCommand(new GFI());
}
if (fti.isUpload()) {
addCommand(nmdc?new ADCGET():new GET());
}
increaseLoginLevel();
}
/**
* if one of the 3 needed things occur this is called
* so onLogin can be triggered..
*
* @throws IOException
*/
void increaseLoginLevel() throws IOException {
loginLevel++;
if (loginLevel == loginTargetLevel) {
onLogIn();
}
}
/**
* queues a raw message for sending to the client
* @param message
*/
void sendUnmodifiedRaw(final String message) {
super.sendRaw(message);
debugMessages.add("out: "+message);
logger.debug("sent raw: "+message);
}
void sendUnmodifiedRaw(byte[] bytes) {
super.sendRaw(bytes);
}
/**
* closes the connection and prints
* DisconnectReason to the screen
* @param reason - why we closed the connection
*/
public void disconnect(DisconnectReason reason) {
disconnect(reason.toString());
}
/**
* the statis message already sent
* @param adcs -
*/
public void disconnect(ADCStatusMessage adcs) {
disconnect(adcs.toString());
}
private void disconnect(String sreason) {
if (getUser() != null) {
logger.debug("disconnected: "+getUser().getNick()+" reason: "+sreason);
}
disconnectReason = sreason;
if (connection != null) {
logger.debug("closing connection: "+disconnectReason);
if (fileTransfer != null) {
fileTransfer.cancel();
} else {
// connection.flush(500); //for up to half a second we try flushing.. so MaxedOut can get through
logger.debug("closed Connection: "+connection.getClass().getSimpleName());
connection.close();
}
}
}
public void otherSentError(String reason) {
this.disconnectReason = reason;
if (connection != null) {
connection.close();
}
}
/**
* sends an error containing the provided disconnect reason
* to the other user afterwards disconnect(reason)
* is called
*
* @param error - what error occurred
* @throws IOException
*/
void sendError(DisconnectReason error) throws IOException {
if (!error.isError()) {
throw new IllegalArgumentException(
"error must be an error not just an ordinary disconnectreason"+error);
}
if (nmdc) {
Error.sendError(this, error);
}
disconnect(error);
}
/**
* called by protocol commands when the other user is identified
* sets the other user and tries to resolve a
* DownloadQueueEntry to this user.
* @param other
*/
void otherIdentified(IUser other) {
if (isEncrypted() && other.getKeyPrint() != null) {
connection.setFingerPrint(other.getKeyPrint());
}
fti.setOther(other);
if (other != null) {
self = other.getHub().getSelf();
AbstractDownloadQueueEntry dqe = other.resolveDQEToUser();
fti.setDqe(dqe);
other.setIp(otherip);
other.addTransfer(this);
}
}
public IUser getUser() {
return fti.getOther();
}
/**
* starts an download or a download
* if everything in FTI is set
*/
void transfer() throws IOException {
logger.debug("in transfer()");
boolean successful = fti.setFileInterval();
long current = fti.getStartposition();
if (current != 0 && current == lastRequestpos) {
if (++lastRequestCounter > 10) {
successful = false; //user tried to re-download the same part to often -> may be we have different Hash for it -> tell him we don't have that..
logger.debug("not available due lastrequest failed timeout "+fti);
}
} else {
lastRequestpos = current;
lastRequestCounter = 0;
}
if (successful) {
getSlotAndDoTransfer();
} else {
//could no create fileInterval -> file is not available
if (nmdc) {
sendError( DisconnectReason.FILENOTAVAILABLE );
} else {
STA.sendSTA(this,
new ADCStatusMessage(DisconnectReason.FILENOTAVAILABLE.toString(),
ADCStatusMessage.FATAL,
ADCStatusMessage.TransferFileNotAvailable));
}
}
}
private void getSlotAndDoTransfer() {
DCClient dcc = ch.getDCC();
if (fti.isDownload() || (slot = ch.getSlotManager().getSlot(fti.getOther(),fti.getType(), fti.getFileListFile())) != null) { //get a slot for the upload
// Thread transfer = new Thread(new Runnable() {
// public void run() {
// WriteLock l = ClientProtocol.this.writeLock();
// l.lock();
// try {
// runTransfer();
// } finally {
// l.unlock();
// }
// }
// },(fti.isDownload()?"Download: ":"Uupload: ")+fti.getOther().getNick());
// transfer.setDaemon(true);
// transfer.start();
runTransfer();
} else {
//being here means this is an upload and no slots are free.
//send No slots signal
int queuePosition = ch.getSlotManager().getPositionInQueue(fti.getOther());
if (nmdc) {
MaxedOut.sendMaxedOut(this,queuePosition);
} else {
STA.sendSTA(this,
new ADCStatusMessage(DisconnectReason.NOSLOTS.toString()
,ADCStatusMessage.FATAL
,ADCStatusMessage.TransferSlotsFull
,Flag.QP, queuePosition));
//here ADC no slots.. //STA maxed out..
}
dcc.getUpQueue().userRequestedFile(fti.getOther(), fti.getNameOfTransferred(),fti.getHashValue(), false);
}
}
private void runTransfer() {
DCClient dcc = ch.getDCC();
ByteChannel source = null;
try {
if (fti.isUpload()) {
logger.debug("transfer() is an upload..now sending ADCSND");
if (nmdc) {
ADCSND.sendADCSND(this);
} else {
SND.sendADCSND(this);
}
dcc.getUpQueue().userRequestedFile(
fti.getOther(), fti.getNameOfTransferred(),fti.getHashValue(), true);
}
fileTransfer = fti.create(this);
if (getState().isOpen()) {
setState(ConnectionState.TRANSFERSTARTED);
} else {
if (Platform.inDevelopmentMode()) {
logger.warn("Problem transfering: "+getUser()+ " "+getState());
}
throw new IOException();
}
ch.notifyOfChange(ConnectionHandler.TRANSFER_STARTED,this,fileTransfer);
if (fti.isDownload()) { //Register download.. with DQE..
fti.getDqe().startedDownload(fileTransfer);
}
transferCreateTimeOut.cancelScheduled();
// logger.info("Canceling transferCreateTimeOut: tf "+toString());
logger.debug("transfer() start transferring data..");
source = connection.retrieveChannel();
fileTransfer.transferData(source);
logger.debug("transfer() finished transferring data..");
immediateReconnect = true;
} catch(IOException ioe) {
logger.debug("transfer broke with ioexception "+ioe);
} catch(IllegalStateException ise) {
logger.log(Platform.inDevelopmentMode()?Level.WARN:Level.DEBUG, "stupid state exception",ise);
} finally {
boolean wasDownload = false ;
if (fileTransfer != null) {
wasDownload = fileTransfer.isDownload();
dcc.getUpDownQueue(fileTransfer.isUpload()).transferFinished(
fti.getFile(),
fti.getOther(),
fti.getNameOfTransferred(),
fti.getHashValue(),
fileTransfer.getBytesTransferred(),
fileTransfer.getStartTime(),
System.currentTimeMillis()-fileTransfer.getStartTime().getTime(),
getOtherip());
if (slot != null) { //upload
ch.getSlotManager().returnSlot(slot,fti.getOther());
slot = null;
transferCreateTimeOut.reschedule(10, TimeUnit.SECONDS);
} else {
// logger.info("Finished Download: "+fti.getOther().getNick());
}
ch.notifyOfChange(ConnectionHandler.TRANSFER_FINISHED,this,fileTransfer);
if (getState().isOpen()) {
setState(ConnectionState.TRANSFERFINISHED);
} else if (Platform.inDevelopmentMode()) {
logger.warn("Problem after transfering: "+getUser()+ " "+getState());
}
//timerTransferCreateTimeout = 0;
fileTransfer = null;
}
boolean returnSuccessful = false;
if (source != null) {
returnSuccessful = connection.returnChannel(source);
}
if (debugMessages.size() > 2000) {
//check error... too many debug messages..
if (Platform.inDevelopmentMode()) {
logger.warn("Debug Messages count too large "+debugMessages.size());
}
connection.close();
} else if (wasDownload && returnSuccessful && getState().isOpen()) {
logger.debug("relogin");
transferCreateTimeOut.reschedule(20, TimeUnit.SECONDS);
relogin();
}
// if (wasDownload) {
// logger.info("return: "+returnSuccessful + " state: "+getState());
// }
logger.debug("transfer() finished was download"+wasDownload+" return succ:"+returnSuccessful);
}
}
// /**
// * timer used to
// */
// public void timer() {
//
// if (!isLoginDone() && ++timerLogintimeout > 60) { // 60 seconds login timeout
// logger.debug("disconnect sent from here");
// disconnect(DisconnectReason.CONNECTIONTIMEOUT);
// }
//
// // 30 seconds Timeout until a transfer needs to be created.. else we disconnect..
// if ((fileTransfer == null|| !fileTransfer.hasStarted()) && ++timerTransferCreateTimeout > 30) {
// logger.debug("disconnect sent from here2");
// disconnect(DisconnectReason.CONNECTIONTIMEOUT);
// }
//
// //timeout for if we expect an ADCGet from the other but don't get one..
// if (getAwaited && ++awaitingADCGet > 20) {
// logger.debug("disconnect sent from here3");
// disconnect(DisconnectReason.CONNECTIONTIMEOUT);
// }
// }
/**
* @return the disconnectReason
*/
public String getDisconnectReason() {
return disconnectReason;
}
public InetAddress getOtherip() {
return otherip;
}
/**
*
* @return true if on our side was the serversocket
*/
boolean isServer() {
return incoming;
}
public ConnectionHandler getCh() {
return ch;
}
/**
* @return the User representing us in the hub of the other..
*/
IUser getSelf() {
return self;
}
/**
* lottery number that decides who uploads and who Downloads
* who ever has the higher number will download if both are
* interested in downloading
*/
int getMyNumber() {
return myNumber;
}
void setOthersSupports(Set<String> othersSupports) throws IOException {
this.othersSupports = othersSupports;
increaseLoginLevel();
}
void setOthersNumber(int othersNumber) {
this.othersNumber = othersNumber;
}
boolean isLocal() {
return connection.isLocal();
}
public FileTransferInformation getFti() {
return fti;
}
public IFileTransfer getFileTransfer() {
return fileTransfer;
}
public IConnection getConnection() {
return connection;
}
boolean isImmediateReconnect() {
return immediateReconnect;
}
Set<String> getOthersSupports() {
return othersSupports;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String toString() {
if (getUser() == null) {
if (getOtherip() != null) {
return "Connection with: "+getOtherip().toString();
} else {
return "unknown connection";
}
} else {
return "Connection with: "+getUser().toString();
}
}
public IDownloadable getDownloadable() {
if (getFti() != null) {
//for downloads
if (getFti().getDqe() != null) {
return getFti().getDqe().downloadableData();
}
//for uploads
if (getFti().getHashValue() != null) {
return ch.getDCC().getOwnFileList().search(getFti().getHashValue());
}
}
return null;
}
public String getHubaddy() {
return hubaddy;
}
public void setHubaddy(String hubaddy) {
this.hubaddy = hubaddy;
}
}