package uc;
import helpers.GH;
import helpers.Observable;
import helpers.StatusObject;
import helpers.StatusObject.ChangeType;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import logger.LoggerFactory;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.Platform;
import uc.IStoppable.IStartable;
import uc.Identity.FilteredChangedAttributeListener;
import uc.crypto.HashValue;
import uc.protocols.AbstractConnection;
import uc.protocols.CPType;
import uc.protocols.MultiStandardConnection;
import uc.protocols.MultiStandardConnection.ISocketReceiver;
import uc.protocols.client.ClientProtocolStateMachine;
import uc.protocols.client.ClientProtocol;
import uc.protocols.client.ClientProtocolStateMachine.CPSMManager;
import uc.protocols.hub.ICTMListener;
/**
*
* connection handler manages the TCP ServerSocket that receives incoming
* TCP connections it also creates outgoing connections to users that are
* interesting for us (shares something we want) and to users that explicitly
* request us to connect to them i.e. by sending an CTM
*
* @author <b>Quicksilver </b>
*
*/
public class ConnectionHandler extends Observable<StatusObject>
implements ICTMListener , IUserChangedListener ,IStartable {
private static final Logger logger = LoggerFactory.make();
public static final int USER_IDENTIFIED_IN_CONNECTION = 1, //add connection rem ProtocolStatemachine if present
TRANSFER_STARTED = 2, //connection remove -> add Transfer
//Transfer changes are given directly over a different listener..
TRANSFER_FINISHED = 3, //remove Transfer -> addConnection
CONNECTION_CLOSED = 4, //rem connection -> add CPSM if present
STATEMACHINE_CREATED = 5, // no connection just the statemachine got created yet
STATEMACHINE_DESTROYED = 6, // no connection . statemachine finished
STATEMACHINE_CHANGED = 7; //statemachine changed..
private final Set<Object> active =
Collections.synchronizedSet(new HashSet<Object>());
private final Observable<StatusObject> fileTransferObservable = new Observable<StatusObject>();
private final FilteredChangedAttributeListener pca;
private class ServerSocketInfo {
private final boolean encrypted;
public ServerSocketInfo(boolean encrypted) {
this.encrypted = encrypted;
}
private ServerSocketChannel ssc;
private SelectionKey selKey;
private volatile boolean running;
int getPort() {
return identity.getInt(encrypted ? PI.tlsPort: PI.inPort);
}
void changePort() {
close();
register(this);
}
void close() {
if (selKey != null) {
selKey.cancel();
}
GH.close(ssc);
running = false;
}
}
private final ServerSocketInfo
normal = new ServerSocketInfo(false),
tls = new ServerSocketInfo(true);
/**
* users that are interesting
* ie have something... that we want from them
* (if we are active this should contain the same users as
* (expectedToConnect - all passive user that want to connect to us))
*
* the mapping goes to the NMDCC that is responsible for makeing a connect to the user
*
*/
private final Map<IUser,ClientProtocolStateMachine> interesting =
Collections.synchronizedMap(new HashMap<IUser,ClientProtocolStateMachine>());
/**
*
* maps userid to a User that we expect to connect to us (because we have sent a CTM to them)
*
* this is a minor pack-ratting problem .. no users are ever removed from this Set
* though dumps show that this is no problem..
*
*/
private final Set<ExpectedInfo> expectedToConnect =
Collections.synchronizedSet(new HashSet<ExpectedInfo>());
private ScheduledFuture<?> expectedRefresher;
private final DCClient dcc;
private final CPSMManager cpsmManager;
private final Identity identity;
public Identity getIdentity() {
return identity;
}
/**
*
*/
public ConnectionHandler(DCClient dcclient,Identity identityx) {
this.dcc = dcclient;
this.identity = identityx;
cpsmManager = new CPSMManager(dcc);
//register a port changed listener with the settings
pca = new FilteredChangedAttributeListener(PI.inPort,PI.bindAddress,PI.tlsPort) {
@Override
public void preferenceChanged(String preference, String oldValue,String newValue) {
if (PI.inPort.equals(preference) || PI.bindAddress.equals(preference)) {
normal.changePort();
}
if ((PI.tlsPort.equals(preference) || PI.bindAddress.equals(preference)) &&
identity.getCryptoManager().isTLSInitialized()) {
tls.changePort();
}
}
};
}
public void start() {
register(normal);
if (identity.getCryptoManager().isTLSInitialized()) {
register(tls);
}
identity.addObserver(pca);
cpsmManager.start();
dcc.getPopulation().registerUserChangedListener(this);
expectedRefresher = dcc.getSchedulerDir().scheduleWithFixedDelay(new Runnable() {
public void run() {
synchronized(expectedToConnect) {
for (Iterator<ExpectedInfo> it = expectedToConnect.iterator();it.hasNext();) {
if (it.next().isOld()) {
it.remove();
}
}
}
}
}, 60, 60, TimeUnit.SECONDS);
}
public void stop() {
normal.close();
tls.close();
identity.deleteObserver(pca);
cpsmManager.stop();
dcc.getPopulation().unregisterUserChangedListener(this);
if (expectedRefresher != null) {
expectedRefresher.cancel(false);
expectedToConnect.clear();
}
//TOOD close all running transfers -> CPSM should stay..?
}
public void changed(UserChangeEvent uce) {
IUser usr = uce.getChanged();
switch (uce.getType()) {
case CONNECTED:
if (usr.weWantSomethingFromUser()) {
logger.debug("connected user interesting");
onInterestingUserArrived(usr);
}
break;
case CHANGED:
switch(uce.getDetail()) {
case UserChangeEvent.DOWNLOADQUEUE_ENTRY_PRE_ADD_FIRST:
logger.debug("preadd first user interesting");
onInterestingUserArrived(usr);
break;
}
break;
}
}
public boolean isTLSRunning() {
return tls.running;
}
/**
* function to add a user to the expected to connect list..
* when ever we send a CTM to a user we put im in here
* so we know that he is expected to connect..
*
* @param usr the user we expect to connect
* @param protocol the protocol for this connection null in NMDC
* @param token - the token for this connection - null in NMDC
*
*/
public void ctmSent(IUser target, CPType protocol, String token) {
ExpectedInfo ei = new ExpectedInfo(target,protocol,token);
synchronized(expectedToConnect) {
expectedToConnect.remove(ei);
expectedToConnect.add(ei);
}
}
/**
* NMDC function
*
* tries to verify user against given IP
* if no nick matches the IP a random user is returned..
*
* @param nick - the nick the user sent in CTM
* @param ip the IP the connection has
* @return the user that matches nick and if possible also IP
*/
public IUser getUserExpectedToConnect(String nick, InetAddress ip) {
if (ip == null) {
throw new IllegalArgumentException();
}
ArrayList<ExpectedInfo> possibleUsers = new ArrayList<ExpectedInfo>(1);
synchronized(expectedToConnect) {
for (ExpectedInfo ei : expectedToConnect) {
IUser usr = ei.getUser();
if (nick.equals(usr.getNick())) {
if (ip.equals(usr.getIp()) && usr.resolveDQEToUser() != null && !ei.isRemoved()) {
possibleUsers.clear();
possibleUsers.add(ei);
break;
} else {
possibleUsers.add(ei);
}
}
}
ExpectedInfo found = GH.getRandomElement(possibleUsers);
if (found != null) {
found.remove();
return found.getUser();
} else {
if (Platform.inDevelopmentMode()) {
logger.warn("not found CTM info for "+nick+" user was not expected to connect!");
}
return null;
}
}
}
/**
* user determination should be done by token not by CID
*
* @param id
* @return
*/
public ExpectedInfo getUserExpectedToConnect(HashValue cid,String token) {
if (token == null) {
throw new IllegalArgumentException();
}
synchronized(expectedToConnect) {
for (ExpectedInfo ei : expectedToConnect) {
if (token.equals(ei.token)) {
if (cid == null || cid.equals(ei.user.getCID())) {
ei.remove();
return ei;
}
}
}
}
return null;
}
/**
* CHListens here for ctms that were received..
* and opens a Socket to the appropriate address
*/
public void ctmReceived(IUser self , InetSocketAddress isa,IUser other,CPType protocol,String token) {
if (dcc.getFilelist().isInitialized()) {
ClientProtocol ctcp = new ClientProtocol( isa ,this , self,other,
protocol,token , protocol.isEncrypted());
ctcp.start();
}
}
/**
* whenever a user connects this method is called to check if
* we want something of the user.
* <p>
* method is also called when the first DownloadQueueEntry is added
* to the users DQE list.
*
* @param usr the user that should be added
* if he is interesting (has something we want)
*
*/
public void onInterestingUserArrived(final IUser usr) {
if (usr.weWantSomethingFromUser() && !interesting.containsKey(usr)) {
dcc.executeDir(new Runnable() {
public void run() {
new ClientProtocolStateMachine(usr,ConnectionHandler.this);
}
});
}
}
/**
*
* @return the local port of the ConnectionHandler
*/
public int getPort(boolean tlsPort) {
ServerSocketInfo ssi = tlsPort? tls:normal;
if (ssi.ssc != null && ssi.ssc.isOpen()) {
return ssi.ssc.socket().getLocalPort();
} else {
return ssi.getPort();
}
}
/**
* this handles the incoming connections
*/
public void register(final ServerSocketInfo ssi) {
//while(ssi.running) {
try {
ssi.running = true;
ssi.ssc = ServerSocketChannel.open();
AbstractConnection.bindSocket( ssi.ssc, ssi.getPort());
ssi.ssc.configureBlocking(false);
MultiStandardConnection.get().register(ssi.ssc, new ISocketReceiver() {
public void socketReceived(ServerSocketChannel port,SocketChannel created) {
try {
identity.getConnectionDeterminator().connectionReceived(ssi.encrypted);
created.configureBlocking(false);
ClientProtocol cp = new ClientProtocol(created,ConnectionHandler.this,ssi.encrypted);
cp.start();
} catch(IOException ioe) {
logger.error(ioe,ioe);
}
}
public void setKey(SelectionKey key) {
ssi.selKey = key;
}
});
dcc.logEvent(String.format(
ssi.encrypted ?LanguageKeys.StartedEncConnectionHandler
:LanguageKeys.StartedConnectionHandler
, ssi.ssc.socket().getLocalPort()));// "Started "+(ssi.encrypted?"encrypted":"normal")+ " connection handler on TCP-Port: "+ssi.ssc.socket().getLocalPort());
} catch(ClosedByInterruptException ie) {
logger.debug("Connection handler socket closed by interruption");
ssi.close();
} catch(BindException be) {
logger.error(String.format(LanguageKeys.TCPPortInUse, ssi.getPort()),be);// "TCP port %d in use! Change TCP port!",be);
ssi.close();
} catch (IOException e) {
logger.warn("error in serversock "+e,e);
ssi.close();
}
}
public void notifyOfChange(int detail,ClientProtocol cp,Object other) {
StatusObject so = new StatusObject(cp,ChangeType.CHANGED,detail,other );
switch(detail) {
case USER_IDENTIFIED_IN_CONNECTION:
active.add(cp);
if (other != null) {
active.remove(other);
}
break;
case CONNECTION_CLOSED:
active.remove(cp);
ClientProtocolStateMachine cpsm = (ClientProtocolStateMachine)other;
if (cpsm != null && cpsm.isActive()) {
active.add(other);
}
break;
case STATEMACHINE_CREATED:
active.add(other);
break;
case STATEMACHINE_DESTROYED:
active.remove(other);
break;
}
notifyObservers(so);
}
public ClientProtocolStateMachine getStateMachine(IUser usr) {
return interesting.get(usr);
}
public void removeStatemachine(IUser usr,ClientProtocolStateMachine cpsm) {
if (usr == null) {
throw new IllegalArgumentException();
}
//ClientProtocolStateMachine ccps =
interesting.remove(usr);
notifyOfChange(STATEMACHINE_DESTROYED, null, cpsm);
deleteObserver(cpsm);
}
public void addStatemachine(IUser usr,ClientProtocolStateMachine ccps) {
interesting.put(usr, ccps);
addObserver(ccps);
notifyOfChange(STATEMACHINE_CREATED, null, ccps);
}
/**
* returns active elements for the GUI ..
* a mix of ClientProtocols and StateMachine..
* @return
*/
public Set<Object> getActive() {
return active;
}
public int getNrOfRunningDownloads() {
int i = 0;
synchronized (active) {
for (Object o: active) {
if (o instanceof ClientProtocol) {
ClientProtocol cp =(ClientProtocol)o;
if (cp.getFti().isDownload()) {
i++;
}
}
}
}
return i;
}
public DCClient getDCC() {
return dcc;
}
public ISlotManager getSlotManager() {
return dcc.getSlotManager();
}
public CPSMManager getCpsmManager() {
return cpsmManager;
}
public void addTransferObserver(helpers.Observable.IObserver<StatusObject> o) {
fileTransferObservable.addObserver(o);
}
public void deleteTransferObserver(IObserver<StatusObject> o) {
fileTransferObservable.deleteObserver(o);
}
public void notifyTransferObservers(StatusObject arg) {
fileTransferObservable.notifyObservers(arg);
}
/**
* bundles of information holding info for us of users that are expected
* to connect to us
*
* @author Quicksilver
*
*/
public static class ExpectedInfo {
private static final long OLD_MILLIS = 5 *60 *1000;
private final IUser user;
private final CPType protocol;
private final String token;
private final long created;
private boolean removed = false;
public ExpectedInfo(IUser user,CPType protocol, String token) {
this.protocol = protocol;
this.token = token;
this.user = user;
this.created = System.currentTimeMillis();
}
public IUser getUser() {
return user;
}
public boolean isOld() {
return System.currentTimeMillis() - created > OLD_MILLIS;
}
public synchronized boolean isRemoved() {
return removed;
}
public synchronized void remove() {
removed = true;
}
public CPType getProtocol() {
return protocol;
}
public String getToken() {
return token;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((protocol == null) ? 0 : protocol.hashCode());
result = prime * result + ((token == null) ? 0 : token.hashCode());
result = prime * result + ((user == null) ? 0 : user.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ExpectedInfo other = (ExpectedInfo) obj;
if (protocol != other.protocol)
return false;
if (token == null) {
if (other.token != null)
return false;
} else if (!token.equals(other.token))
return false;
if (user == null) {
if (other.user != null)
return false;
} else if (!user.equals(other.user))
return false;
return true;
}
}
}