package org.limewire.core.impl.friend;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.limewire.friend.api.FriendException;
import org.limewire.friend.api.FriendPresence;
import org.limewire.friend.api.feature.ConnectBackRequestFeature;
import org.limewire.friend.api.feature.FeatureTransport;
import org.limewire.friend.impl.address.FriendAddressResolver;
import org.limewire.friend.impl.address.FriendFirewalledAddress;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.Address;
import org.limewire.io.Connectable;
import org.limewire.io.GUID;
import org.limewire.io.IOUtils;
import org.limewire.io.NetworkUtils;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.net.ConnectBackRequest;
import org.limewire.net.SocketsManager;
import org.limewire.net.address.AddressConnector;
import org.limewire.net.address.FirewalledAddress;
import org.limewire.nio.AbstractNBSocket;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.rudp.UDPSelectorProvider;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.NetworkManager;
import com.limegroup.gnutella.SocketProcessor;
import com.limegroup.gnutella.downloader.PushDownloadManager;
import com.limegroup.gnutella.downloader.PushedSocketHandler;
import com.limegroup.gnutella.downloader.PushedSocketHandlerRegistry;
/**
* Connects an {@link org.limewire.friend.impl.address.FriendFirewalledAddress} and tries to get a socket for it.
*/
@EagerSingleton
class FriendFirewalledAddressConnector implements AddressConnector, PushedSocketHandler {
private static final Log LOG = LogFactory.getLog(FriendFirewalledAddressConnector.class, LOGGING_CATEGORY);
private final PushDownloadManager pushDownloadManager;
private final NetworkManager networkManager;
private final ScheduledExecutorService backgroundExecutor;
final List<PushedSocketConnectObserver> observers = new CopyOnWriteArrayList<PushedSocketConnectObserver>();
private final Provider<UDPSelectorProvider> udpSelectorProvider;
private final Provider<SocketProcessor> socketProcessor;
private final FriendAddressResolver friendAddressResolver;
@Inject
public FriendFirewalledAddressConnector(FriendAddressResolver friendAddressResolver, PushDownloadManager pushDownloadManager,
NetworkManager networkManager, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
Provider<UDPSelectorProvider> udpSelectorProvider,
Provider<SocketProcessor> socketProcessor) {
this.friendAddressResolver = friendAddressResolver;
this.pushDownloadManager = pushDownloadManager;
this.networkManager = networkManager;
this.backgroundExecutor = backgroundExecutor;
this.udpSelectorProvider = udpSelectorProvider;
this.socketProcessor = socketProcessor;
}
@Inject
void register(SocketsManager socketsManager) {
socketsManager.registerConnector(this);
}
@Inject
void register(PushedSocketHandlerRegistry pushedSocketHandlerRegistry) {
pushedSocketHandlerRegistry.register(this);
}
@Override
public boolean canConnect(Address address) {
if (address instanceof FriendFirewalledAddress) {
FriendFirewalledAddress friendFirewalledAddress = (FriendFirewalledAddress)address;
// let push download manager decide if we're locally capabable of connecting
boolean canConnect = pushDownloadManager.canConnect(friendFirewalledAddress.getFirewalledAddress());
LOG.debugf("{0} connect remote address {1}, because PDM cannot connect {2}", (canConnect ? "can" : "can not"), address, friendFirewalledAddress.getFirewalledAddress());
return canConnect;
}
LOG.debugf("can not connect remote address {0}", address);
return false;
}
@Override
public void connect(Address address, ConnectObserver observer) {
try {
connectSendingConnectBack(address, observer);
} catch (ConnectBackRequestException ce) {
LOG.debugf(ce, "could not send connect back request {0}", address);
// fall back on push download manager
pushDownloadManager.connect(((FriendFirewalledAddress)address).getFirewalledAddress(), observer);
}
}
/**
* @throws ConnectBackRequestException if sending the connect back request fails
*/
void connectSendingConnectBack(Address address, ConnectObserver observer) throws ConnectBackRequestException {
FriendFirewalledAddress friendFirewalledAddress = (FriendFirewalledAddress)address;
FirewalledAddress firewalledAddress = friendFirewalledAddress.getFirewalledAddress();
GUID clientGuid = firewalledAddress.getClientGuid();
Connectable publicAddress = networkManager.getPublicAddress();
if (!NetworkUtils.isValidIpPort(publicAddress)) {
LOG.debugf("not a valid public address yet: {0}", publicAddress);
observer.handleIOException(new ConnectException("no valid address yet: " + publicAddress));
return;
}
boolean isFWT = !networkManager.acceptedIncomingConnection();
FriendPresence presence = friendAddressResolver.getPresence(friendFirewalledAddress.getFriendAddress());
if (presence == null) {
throw new ConnectBackRequestException("no presence available for: " + friendFirewalledAddress.getFriendAddress());
}
FeatureTransport<ConnectBackRequest> transport = presence.getTransport(ConnectBackRequestFeature.class);
if (transport == null) {
throw new ConnectBackRequestException("no transport for presence: " + presence);
}
/* there's a slight race condition, if a connection was just accepted between getting the address
* and checking for it in the call below, but this should only change the address wrt to port vs
* udp port which are usually the same anyways.
*/
final PushedSocketConnectObserver pushedSocketObserver = new PushedSocketConnectObserver(firewalledAddress, observer);
observers.add(pushedSocketObserver);
try {
transport.sendFeature(presence, new ConnectBackRequest(publicAddress, clientGuid, isFWT ? networkManager.supportsFWTVersion() : 0));
} catch (FriendException e) {
// clean up observer
observers.remove(pushedSocketObserver);
throw new ConnectBackRequestException(e);
}
if (isFWT) {
LOG.debug("Starting fwt communication");
assert NetworkUtils.isValidIpPort(firewalledAddress.getPublicAddress()) : "invalid public address" + firewalledAddress;
AbstractNBSocket socket = udpSelectorProvider.get().openSocketChannel().socket();
socket.connect(firewalledAddress.getPublicAddress().getInetSocketAddress(), 20000, new ConnectObserver() {
@Override
public void handleConnect(Socket socket) throws IOException {
LOG.debugf("handling socket: {0}", socket);
// have to route connected socket through socket processor and PushDownloadManager
// so parsing of the GIV line is taken care of
socketProcessor.get().processSocket(socket, "GIV");
}
@Override
public void handleIOException(IOException iox) {
pushedSocketObserver.handleIOException(iox);
}
@Override
public void shutdown() {
pushedSocketObserver.handleIOException(new IOException("shutdown"));
}
});
} else {
// wait for the other side to open a TCP connection to this peer
}
scheduleExpirerFor(pushedSocketObserver, 30 * 1000);
}
private void scheduleExpirerFor(final PushedSocketConnectObserver pushedSocketObserver, int timeout) {
backgroundExecutor.schedule(new Runnable() {
@Override
public void run() {
observers.remove(pushedSocketObserver);
pushedSocketObserver.handleTimeout();
}
}, timeout, TimeUnit.MILLISECONDS);
}
@Override
public boolean acceptPushedSocket(String file, int index, byte[] clientGUID, Socket socket) {
for (PushedSocketConnectObserver observer: observers) {
if (observer.acceptSocket(clientGUID, socket)) {
return true;
}
}
return false;
}
/**
* Keeps connection state around and notifies the original {@link ConnectObserver}
* of failures or success, ensuring that only one event is reported to it.
*/
static class PushedSocketConnectObserver {
private final FirewalledAddress firewalledAddress;
private final ConnectObserver observer;
final AtomicBoolean acceptedOrFailed = new AtomicBoolean(false);
public PushedSocketConnectObserver(FirewalledAddress firewalledAddress, ConnectObserver observer) {
this.firewalledAddress = firewalledAddress;
this.observer = observer;
}
public boolean acceptSocket(byte[] clientGuid, Socket socket) {
if (Arrays.equals(clientGuid, firewalledAddress.getClientGuid().bytes())) {
Connectable expectedAddress = firewalledAddress.getPublicAddress();
if (NetworkUtils.isValidIpPort(expectedAddress) && !expectedAddress.getInetAddress().equals(socket.getInetAddress())) {
LOG.debugf("received socket from unexpected location, expected: {0}, actual: {1}", expectedAddress, socket);
return false;
}
if (acceptedOrFailed.compareAndSet(false, true)) {
try {
LOG.debugf("handling connect from: {0}", socket);
observer.handleConnect(socket);
} catch (IOException ie) {
IOUtils.close(socket);
}
return true;
}
}
return false;
}
public void handleTimeout() {
LOG.debug("handling timeout");
handleIOException(new ConnectException("connect request timed out"));
}
public void handleIOException(IOException ie) {
LOG.debug("handling io exception", ie);
if (acceptedOrFailed.compareAndSet(false, true)) {
observer.handleIOException(ie);
}
}
}
static class ConnectBackRequestException extends Exception {
public ConnectBackRequestException(String message) {
super(message);
}
public ConnectBackRequestException(Throwable cause) {
super(cause);
}
}
}