package com.limegroup.gnutella.downloader;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.DefaultedHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.Address;
import org.limewire.io.Connectable;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.GUID;
import org.limewire.io.IOUtils;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortImpl;
import org.limewire.io.NetworkUtils;
import org.limewire.listener.EventBroadcaster;
import org.limewire.listener.ListenerSupport;
import org.limewire.listener.RegisteringEventListener;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.net.ConnectionAcceptor;
import org.limewire.net.ConnectivityChangeEvent;
import org.limewire.net.SocketsManager;
import org.limewire.net.address.AddressConnector;
import org.limewire.net.address.AddressEvent;
import org.limewire.net.address.FirewalledAddress;
import org.limewire.nio.AbstractNBSocket;
import org.limewire.nio.channel.AbstractChannelInterestReader;
import org.limewire.nio.channel.NIOMultiplexor;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.rudp.UDPSelectorProvider;
import org.limewire.util.Base32;
import org.limewire.util.BufferUtils;
import org.limewire.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.MessageRouter;
import com.limegroup.gnutella.NetworkManager;
import com.limegroup.gnutella.PushEndpoint;
import com.limegroup.gnutella.PushEndpointCache;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.SocketProcessor;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HttpClientListener;
import com.limegroup.gnutella.http.HttpExecutor;
import com.limegroup.gnutella.messages.PushRequest;
import com.limegroup.gnutella.messages.PushRequestImpl;
import com.limegroup.gnutella.messages.Message.Network;
import com.limegroup.gnutella.util.MultiShutdownable;
import com.limegroup.gnutella.util.URLDecoder;
/**
* Handles sending out pushes and awaiting incoming GIVs.
*/
@EagerSingleton
public class PushDownloadManager implements ConnectionAcceptor, PushedSocketHandlerRegistry, AddressConnector, RegisteringEventListener<AddressEvent> {
private static final Log LOG = LogFactory.getLog(PushDownloadManager.class, LOGGING_CATEGORY);
private static final int SPECIAL_INDEX = 0;
/**
* how long we think should take a host that receives an udp push
* to connect back to us.
*/
private static long UDP_PUSH_FAILTIME = 5000;
/** Pool on which blocking HTTP PushProxy requests are handled. */
private final ExecutorService PUSH_THREAD_POOL =
ExecutorsHelper.newFixedSizeThreadPool(10, "PushProxy Requests");
/**
* Number of files that we have sent a udp push for and are waiting a connection.
* LOCKING: obtain UDP_FAILOVER if manipulating the contained sets as well!
*/
private final Map<byte[], AtomicInteger>
UDP_FAILOVER = new TreeMap<byte[], AtomicInteger>(new GUID.GUIDByteComparator());
/**
* The processor to send brand-new incoming sockets to.
* This is different than PushedSocketHandler in that the PushedSocketHandler
* is used after we have read the GIV off the socket (and process the rest of
* the request), whereas this is used to read and ensure it was a GIV.
*/
private final Provider<SocketProcessor> socketProcessor;
private final Provider<HttpExecutor> httpExecutor;
private final Provider<HttpParams> defaultParams;
private final ScheduledExecutorService backgroundExecutor;
private final NetworkManager networkManager;
private final Provider<MessageRouter> messageRouter;
private final Provider<IPFilter> ipFilter;
private final Provider<UDPService> udpService;
private final CopyOnWriteArrayList<PushedSocketHandler> pushHandlers = new CopyOnWriteArrayList<PushedSocketHandler>();
private final Provider<UDPSelectorProvider> udpSelectorProvider;
private final Provider<PushEndpointCache> pushEndpointCache;
private final RemoteFileDescFactory remoteFileDescFactory;
private final EventBroadcaster<ConnectivityChangeEvent> connectivityEventBroadcaster;
private final AtomicBoolean acceptedIncomingConnectionEventFired = new AtomicBoolean(false);
private final AtomicBoolean canDoFWTEventFired = new AtomicBoolean(false);
@Inject
public PushDownloadManager(
Provider<MessageRouter> router,
Provider<HttpExecutor> executor,
@Named("defaults") Provider<HttpParams> defaultParams,
@Named("backgroundExecutor") ScheduledExecutorService scheduler,
Provider<SocketProcessor> processor,
NetworkManager networkManager,
Provider<IPFilter> ipFilter, Provider<UDPService> udpService,
Provider<UDPSelectorProvider> udpSelectorProvider,
Provider<PushEndpointCache> pushEndpointCache,
RemoteFileDescFactory remoteFileDescFactory,
EventBroadcaster<ConnectivityChangeEvent> connectivityEventBroadcaster) {
this.messageRouter = router;
this.httpExecutor = executor;
this.defaultParams = defaultParams;
this.backgroundExecutor = scheduler;
this.socketProcessor = processor;
this.networkManager = networkManager;
this.ipFilter = ipFilter;
this.udpService = udpService;
this.udpSelectorProvider = udpSelectorProvider;
this.pushEndpointCache = pushEndpointCache;
this.remoteFileDescFactory = remoteFileDescFactory;
this.connectivityEventBroadcaster = connectivityEventBroadcaster;
}
public void register(PushedSocketHandler handler) {
pushHandlers.add(handler);
}
@Inject
public void register(SocketsManager socketsManager) {
socketsManager.registerConnector(this);
}
@Inject
public void register(ListenerSupport<AddressEvent> addressEventListenerSupport) {
addressEventListenerSupport.addListener(this);
}
public void handleEvent(AddressEvent event) {
/* Checks if this peer can accept incoming connections and fires
* a ConnectivityChangeEvent if there hasn't been one thrown yet.
*
* Only if incoming connections can't be accepted, reliable udp
* connectivity is considered and an event is thrown if there
* hasn't been one thrown yet.
*/
if (networkManager.acceptedIncomingConnection()) {
if (acceptedIncomingConnectionEventFired.compareAndSet(false, true)) {
connectivityEventBroadcaster.broadcast(new ConnectivityChangeEvent());
}
} else if (networkManager.canDoFWT()) {
if (canDoFWTEventFired.compareAndSet(false, true)) {
connectivityEventBroadcaster.broadcast(new ConnectivityChangeEvent());
}
}
}
public boolean isBlocking() {
return true;
}
/**
* Accepts the given socket for a push download to this host.
* If the GIV is for a file that was never requested or
* has already been downloaded, this will deal with it appropriately.
* In any case this eventually closes the socket.
* <p>
* Non-blocking.
*
* @modifies this
* @requires "GIV " is already read from the socket
*/
public void acceptConnection(String word, Socket socket) {
((NIOMultiplexor)socket).setReadObserver(new GivParser(socket));
}
@Override
public void connect(Address address, ConnectObserver observer) {
if (address instanceof FirewalledAddress) {
connect((FirewalledAddress)address, observer);
} else if (address instanceof PushEndpoint) {
connect((PushEndpoint)address, observer);
}
}
private void connect(PushEndpoint pushEndpoint, ConnectObserver observer) {
RemoteFileDesc fakeRFD =
remoteFileDescFactory.createRemoteFileDesc(pushEndpoint, SPECIAL_INDEX, "fake",
0, pushEndpoint.getClientGUID(), 0, 0, false, null, URN.NO_URN_SET, false, "", -1,
true);
connect(fakeRFD, observer);
}
private void connect(FirewalledAddress address, ConnectObserver observer) {
RemoteFileDesc fakeRFD =
remoteFileDescFactory.createRemoteFileDesc(address, SPECIAL_INDEX, "fake",
0, address.getClientGuid().bytes(), 0, 0, false, null, URN.NO_URN_SET, false, "", -1,
true);
connect(fakeRFD, observer);
}
private void connect(RemoteFileDesc rfd, ConnectObserver observer) {
PushedSocketHandlerAdapter handlerAdapter = new PushedSocketHandlerAdapter(rfd, observer);
pushHandlers.add(handlerAdapter);
sendPush(rfd, handlerAdapter);
scheduleExpirerFor(handlerAdapter, 30 * 1000);
}
private void scheduleExpirerFor(final PushedSocketHandlerAdapter handlerAdapter, int timeout) {
backgroundExecutor.schedule(new Runnable() {
@Override
public void run() {
// always remove after timeout
pushHandlers.remove(handlerAdapter);
handlerAdapter.handleTimeout();
}
}, timeout, TimeUnit.MILLISECONDS);
}
/**
* Sends a push for the given file.
*/
public void sendPush(RemoteFileDesc file) {
sendPush(file, new NullMultiShutdownable());
}
private boolean hasValidLocalAddress() {
//Make sure we know our correct address/port.
// If we don't, we can't send pushes yet.
byte[] addr = networkManager.getAddress();
// TODO check stable udp port if we're interested in fwts?
int port = networkManager.getPort();
return NetworkUtils.isValidAddress(addr) && NetworkUtils.isValidPort(port);
}
/**
* Sends a push request for the given file.
*
* @param file the <tt>RemoteFileDesc</tt> constructed from the query
* hit, containing data about the host we're pushing to
* @param observer the ConnectObserver to notify of success or failure
*/
public void sendPush(RemoteFileDesc file, MultiShutdownable observer) {
if (LOG.isDebugEnabled()) {
LOG.debug("Sending push: " + file);
}
if (!hasValidLocalAddress()) {
LOG.debug("no valid address or port yet");
observer.shutdown();
return;
}
final byte[] guid = GUID.makeGuid();
// If multicast worked, try nothing else.
if (sendPushMulticast(file,guid))
return;
// if we can't accept incoming connections, we can only try
// using the TCP push proxy, which will do fw-fw transfers.
if (!networkManager.acceptedIncomingConnection()) {
// if we can do FWT, offload a TCP pusher.
if (networkManager.canDoFWT())
sendPushTCP(file, guid, observer);
else {
LOG.debug("Firewalled and can't do FWT yet");
observer.shutdown();
}
return;
}
// remember that we are waiting a push from this host
// for the specific file.
// do not send tcp pushes to results from alternate locations.
if (!file.isFromAlternateLocation()) {
addUDPFailover(file);
// schedule the failover tcp pusher, which will run
// if we don't get a response from the UDP push
// within the UDP_PUSH_FAILTIME timeframe
backgroundExecutor.schedule(
new PushFailoverRequestor(file, guid, observer), UDP_PUSH_FAILTIME, TimeUnit.MILLISECONDS);
}
sendPushUDP(file,guid);
}
/**
* Sends a push through multicast.
* <p>
* Returns true only if the RemoteFileDesc was a reply to a multicast query
* and we wanted to send through multicast. Otherwise, returns false,
* as we shouldn't reply on the multicast network.
*/
private boolean sendPushMulticast(RemoteFileDesc file, byte []guid) {
// Send as multicast if it's multicast.
if( file.isReplyToMulticast() ) {
byte[] addr = networkManager.getNonForcedAddress();
int port = networkManager.getNonForcedPort();
if( NetworkUtils.isValidAddress(addr) &&
NetworkUtils.isValidPort(port) ) {
PushRequest pr = new PushRequestImpl(guid,
(byte)1, //ttl
file.getClientGUID(),
file.getIndex(),
addr,
port,
Network.MULTICAST,
networkManager.isIncomingTLSEnabled());
messageRouter.get().sendMulticastPushRequest(pr);
if (LOG.isInfoEnabled())
LOG.info("Sending push request through multicast " + pr);
return true;
}
}
return false;
}
private Set<? extends IpPort> getPushProxies(RemoteFileDesc rfd) {
Address address = rfd.getAddress();
if (address instanceof PushEndpoint) {
return ((PushEndpoint)address).getProxies();
} else if (address instanceof FirewalledAddress) {
return ((FirewalledAddress)address).getPushProxies();
}
return IpPort.EMPTY_SET;
}
private IpPort getPublicAddress(Address address) {
if (address instanceof PushEndpoint) {
IpPort externalAddress = ((PushEndpoint)address).getValidExternalAddress();
return externalAddress != null ? externalAddress : ConnectableImpl.INVALID_CONNECTABLE;
} else if (address instanceof FirewalledAddress) {
return ((FirewalledAddress)address).getPublicAddress();
}
return ConnectableImpl.INVALID_CONNECTABLE;
}
/**
* Sends a push through UDP.
*
* @return true if a push request was sent to at least one push proxy
*/
private boolean sendPushUDP(RemoteFileDesc file, byte[] guid) {
PushRequest pr =
new PushRequestImpl(guid,
(byte)2,
file.getClientGUID(),
file.getIndex(),
networkManager.getAddress(),
networkManager.getPort(),
Network.UDP,
networkManager.isIncomingTLSEnabled());
if (LOG.isInfoEnabled())
LOG.info("Sending push request through udp " + pr);
UDPService udpService = this.udpService.get();
//and send the push to the node
IpPort publicAddress = getPublicAddress(file.getAddress());
//don't bother sending direct push if the node reported invalid
//address and port.
boolean sent = false;
if (NetworkUtils.isValidIpPort(publicAddress)) {
if (LOG.isDebugEnabled()) {
LOG.debug("sending push to host itself via udp: " + publicAddress);
}
udpService.send(pr, publicAddress);
sent = true;
}
//make sure we send it to the proxies, if any
for(IpPort ppi : getPushProxies(file)) {
if (ipFilter.get().allow(ppi.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("sending udp push to: " + ppi);
}
udpService.send(pr, ppi.getInetSocketAddress());
sent = true;
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("removing disallowed pushproxy: " + ppi);
}
removePushProxy(file.getClientGUID(), ppi);
}
}
return sent;
}
/**
* Sends a push through TCP.
* <p>
* This method will always return immediately,
* and the PushConnector will be notified or success or failure.
*/
private void sendPushTCP(RemoteFileDesc file, final byte[] guid, MultiShutdownable observer) {
// if this is a FW to FW transfer, we must consider special stuff
final boolean shouldDoFWTransfer = getFWTVersion(file.getAddress()) > 0 &&
networkManager.canDoFWT() &&
!networkManager.acceptedIncomingConnection();
PushData data = new PushData(observer, file, guid, shouldDoFWTransfer);
// if there are no proxies, send through the network
Set<? extends IpPort> proxies = getPushProxies(file);
if(proxies.isEmpty()) {
sendPushThroughNetwork(data);
return;
}
// Try and send the push through proxies -- if none of the proxies work,
// the PushMessageSender will send it through the network.
sendPushThroughProxies(data, proxies);
}
/**
* Sends a push through the network. The observer is notified upon failure.
*/
private void sendPushThroughNetwork(PushData data) {
LOG.debug("sending through network");
// at this stage, there is no additional shutdownable to notify.
data.getMultiShutdownable().addShutdownable(null);
// if push proxies failed, but we need a fw-fw transfer, give up.
if (data.isFWTransfer() && !networkManager.acceptedIncomingConnection()) {
LOG.debug("through network, FWT and not accepted incoming connection");
data.getMultiShutdownable().shutdown();
return;
}
byte[] addr = networkManager.getAddress();
int port = networkManager.getPort();
if (!NetworkUtils.isValidAddressAndPort(addr, port)) {
LOG.debug("through network, no valid address or port");
data.getMultiShutdownable().shutdown();
return;
}
PushRequest pr = new PushRequestImpl(data.getGuid(),
ConnectionSettings.TTL.getValue(),
data.getFile().getClientGUID(),
data.getFile().getIndex(),
addr,
port,
Network.TCP,
networkManager.isIncomingTLSEnabled());
if (LOG.isInfoEnabled())
LOG.info("Sending push request through Gnutella: " + pr);
try {
messageRouter.get().sendPushRequest(pr);
} catch (IOException e) {
LOG.debug("no route sending through network");
// this will happen if we have no push route.
data.getMultiShutdownable().shutdown();
}
}
/**
* Attempts to send a push through the given proxies. If any succeed,
* the observer will be notified immediately. If all fail, the PushMessageSender
* is told to send the push through the network.
*/
private void sendPushThroughProxies(PushData data, Set<? extends IpPort> proxies) {
if (LOG.isDebugEnabled()) {
LOG.debug("sending through proxies: " + proxies);
}
byte[] externalAddr = networkManager.getExternalAddress();
// if a fw transfer is necessary, but our external address is invalid,
// then exit immediately 'cause nothing will work.
if (data.isFWTransfer() && !NetworkUtils.isValidAddress(externalAddr)) {
LOG.debug("FWT, but no valid external address yet");
data.getMultiShutdownable().shutdown();
return;
}
//TODO: send push msg directly to a proxy if you're connected to it.
if (LOG.isDebugEnabled()) {
LOG.debug("using guid: " + new GUID(data.getFile().getClientGUID()));
}
// set up the request string --
// if a fw-fw transfer is required, add the extra "file" parameter.
final String request = "/gnutella/push-proxy?ServerID=" +
Base32.encode(data.getFile().getClientGUID()) +
(data.isFWTransfer() ? ("&file=" + PushRequest.FW_TRANS_INDEX) : "") +
(networkManager.isIncomingTLSEnabled() ? "&tls=true" : "");
final String nodeString = HTTPHeaderName.NODE.httpStringValue();
final String nodeValue = data.isFWTransfer() ?
NetworkUtils.ip2string(externalAddr) + ":" + networkManager.getStableUDPPort() :
NetworkUtils.ip2string(networkManager.getAddress()) + ":" + networkManager.getPort();
// the methods to execute
final List<HttpHead> methods = new ArrayList<HttpHead>();
// try to contact each proxy
for(IpPort ppi : proxies) {
if (!ipFilter.get().allow(ppi.getAddress())) {
removePushProxy(data.file.getClientGUID(), ppi);
continue;
}
String protocol = "http://";
if(ppi instanceof Connectable) {
if(((Connectable)ppi).isTLSCapable())
protocol = "tls://";
}
String connectTo = protocol + ppi.getAddress() + ":" + ppi.getPort() + request;
HttpHead head = null;
head = new HttpHead(connectTo);
head.addHeader(nodeString, nodeValue);
head.addHeader("Cache-Control", "no-cache");
methods.add(head);
}
if(!methods.isEmpty()) {
HttpClientListener l = new PushHttpClientListener(methods, data);
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 5000);
HttpConnectionParams.setSoTimeout(params, 5000);
params = new DefaultedHttpParams(params, defaultParams.get());
Shutdownable s = httpExecutor.get().executeAny(l, PUSH_THREAD_POOL, methods, params, data.getMultiShutdownable());
data.getMultiShutdownable().addShutdownable(s);
} else {
sendPushThroughNetwork(data);
}
}
private void removePushProxy(byte[] guid, URI uri) {
try {
IpPort pushProxy = new IpPortImpl(uri.getHost(), uri.getPort());
removePushProxy(guid, pushProxy);
} catch (UnknownHostException uhe) {
if (LOG.isWarnEnabled()) {
LOG.warn("exception on host that should have worked", uhe);
}
}
}
private void removePushProxy(byte[] guid, IpPort pushProxy) {
pushEndpointCache.get().removePushProxy(guid, pushProxy);
}
/**
* Listener for callbacks from http requests succeeding or failing.
* This will ensure that only enough proxies are contacted as necessary,
* and send through the network if necessary.
*/
private class PushHttpClientListener implements HttpClientListener {
/** The HttpMethods that are being executed. */
private final Collection<HttpUriRequest> methods;
/** Information about the push. */
private final PushData data;
PushHttpClientListener(Collection<? extends HttpUriRequest> methods, PushData data) {
this.methods = new LinkedList<HttpUriRequest>(methods);
this.data = data;
}
public boolean requestFailed(HttpUriRequest request, HttpResponse response, IOException exc) {
if (LOG.isWarnEnabled()) {
LOG.warn("PushProxy request exception: " + request.getURI(), exc);
}
httpExecutor.get().releaseResources(response);
methods.remove(request);
URI uri = request.getURI();
if (LOG.isDebugEnabled()) {
LOG.debug("Removing push proxy: " + uri + " for "
+ new GUID(data.file.getClientGUID()));
}
removePushProxy(data.file.getClientGUID(), uri);
if (methods.isEmpty()) // all failed
sendPushThroughNetwork(data);
return true;
}
public boolean requestComplete(HttpUriRequest request, HttpResponse response) {
methods.remove(request);
int statusCode = response.getStatusLine().getStatusCode();
httpExecutor.get().releaseResources(response);
if (statusCode == 202) {
if (LOG.isDebugEnabled())
LOG.debug("Successful push proxy: " + request.getURI());
if (data.isFWTransfer()) {
LOG.debug("Starting fwt communication");
AbstractNBSocket socket = udpSelectorProvider.get().openSocketChannel()
.socket();
data.getMultiShutdownable().addShutdownable(socket);
IpPort publicAddress = getPublicAddress(data.getFile().getAddress());
// TODO: see issue: LWC-2278
if (NetworkUtils.isValidIpPort(publicAddress)) {
socket.connect(publicAddress.getInetSocketAddress(), 20000, new FWTConnectObserver(socketProcessor.get()));
}
}
return false; // don't need to process any more methods.
}
if (LOG.isWarnEnabled())
LOG.warn("Invalid push proxy: " + request.getURI() + ", response: "
+ response.getStatusLine().getStatusCode() + ", reason: "
+ response.getStatusLine().getReasonPhrase());
removePushProxy(data.file.getClientGUID(), request.getURI());
if (methods.isEmpty()) // all failed
sendPushThroughNetwork(data);
return true; // try more.
}
@Override
public boolean allowRequest(HttpUriRequest request) {
return true;
}
}
/** Accepts a socket that has had a GIV read off it already. */
void handleGIV(Socket socket, GIVLine line) {
String file = line.file;
int index = 0;
byte[] clientGUID = line.clientGUID;
// if the push was sent through udp, make sure we cancel the failover push.
cancelUDPFailover(clientGUID);
if (LOG.isDebugEnabled()) {
LOG.debug("receiving socket: " + socket + ", line: " + line);
}
boolean accepted = false;
for(PushedSocketHandler handler : pushHandlers) {
if(handler.acceptPushedSocket(file, index, clientGUID, socket)) {
accepted = true;
break;
}
}
if(!accepted) {
IOUtils.close(socket);
}
}
/**
* Adds the necessary data into UDP_FAILOVER so that a PushFailoverRequestor
* knows if it should send a request.
*/
private void addUDPFailover(RemoteFileDesc file) {
synchronized (UDP_FAILOVER) {
byte[] key = file.getClientGUID();
AtomicInteger requests = UDP_FAILOVER.get(key);
if (requests == null) {
requests = new AtomicInteger(0);
UDP_FAILOVER.put(key, requests);
}
requests.addAndGet(1);
}
}
/**
* Removes data from UDP_FAILOVER, indicating a push has used it.
*/
private void cancelUDPFailover(byte[] clientGUID) {
synchronized (UDP_FAILOVER) {
byte[] key = clientGUID;
AtomicInteger requests = UDP_FAILOVER.get(key);
if (requests != null) {
if (requests.decrementAndGet() <= 0)
UDP_FAILOVER.remove(key);
}
}
}
/** A struct-like container storing push information. */
private static class PushData {
private final MultiShutdownable observer;
private final RemoteFileDesc file;
private final byte [] guid;
private final boolean shouldDoFWTransfer;
PushData(MultiShutdownable observer,
RemoteFileDesc file,
byte [] guid,
boolean shouldDoFWTransfer) {
this.observer = observer;
this.file = file;
this.guid = guid;
this.shouldDoFWTransfer = shouldDoFWTransfer;
}
public RemoteFileDesc getFile() {
return file;
}
public byte[] getGuid() {
return guid;
}
public MultiShutdownable getMultiShutdownable() {
return observer;
}
public boolean isFWTransfer() {
return shouldDoFWTransfer;
}
}
/**
* Sends a tcp push if the udp push has failed.
*/
private class PushFailoverRequestor implements Runnable {
final RemoteFileDesc _file;
final byte[] _guid;
final MultiShutdownable connector;
public PushFailoverRequestor(RemoteFileDesc file, byte[] guid, MultiShutdownable connector) {
_file = file;
_guid = guid;
this.connector = connector;
}
public void run() {
if (shouldProceed())
sendPushTCP(_file, _guid, connector);
}
protected boolean shouldProceed() {
byte[] key = _file.getClientGUID();
synchronized (UDP_FAILOVER) {
AtomicInteger requests = UDP_FAILOVER.get(key);
if (requests != null && requests.get() > 0) {
if (requests.decrementAndGet() == 0) {
UDP_FAILOVER.remove(key);
}
return true;
}
}
return false;
}
}
/**
* Non-blocking read-channel to parse the rest of a GIV request
* and hand it off to handleGIV.
*/
private class GivParser extends AbstractChannelInterestReader {
private final Socket socket;
private final StringBuilder givSB = new StringBuilder();
private final StringBuilder blankSB = new StringBuilder();
private boolean readBlank;
private GIVLine giv;
GivParser(Socket socket) {
super(1024);
this.socket = socket;
}
public void handleRead() throws IOException {
// Fill up our buffer as much we can.
while(true) {
int read = 0;
while(buffer.hasRemaining() && (read = source.read(buffer)) > 0);
if(buffer.position() == 0) {
if(read == -1)
close();
break;
}
buffer.flip();
if(giv == null) {
if(BufferUtils.readLine(buffer, givSB))
giv = parseLine(givSB.toString());
}
if(giv != null && !readBlank) {
readBlank = BufferUtils.readLine(buffer, blankSB);
if(blankSB.length() > 0)
throw new IOException("didn't read blank line");
}
buffer.compact();
if(readBlank) {
handleGIV(socket, giv);
break;
}
}
}
private GIVLine parseLine(String command) throws IOException{
//2. Parse and return the fields.
try {
//a) Extract file index. IndexOutOfBoundsException
// or NumberFormatExceptions will be thrown here if there's
// a problem. They're caught below.
int i=command.indexOf(":");
int index=Integer.parseInt(command.substring(0,i));
//b) Extract clientID. This can throw
// IndexOutOfBoundsException or
// IllegalArgumentException, which is caught below.
int j=command.indexOf("/", i);
byte[] guid=GUID.fromHexString(command.substring(i+1,j));
//c). Extract file name.
String filename=URLDecoder.decode(command.substring(j+1));
return new GIVLine(filename, index, guid);
} catch (IndexOutOfBoundsException e) {
throw new IOException();
} catch (NumberFormatException e) {
throw new IOException();
} catch (IllegalArgumentException e) {
throw new IOException();
}
}
}
static final class GIVLine {
final String file;
final int index;
final byte[] clientGUID;
GIVLine(String file, int index, byte[] clientGUID) {
this.file=file;
this.index=index;
this.clientGUID=clientGUID;
}
@Override
public String toString() {
return StringUtils.toString(this);
}
}
/** Simple ConnectObserver for FWT connections. */
private static class FWTConnectObserver implements ConnectObserver {
private final SocketProcessor processor;
FWTConnectObserver(SocketProcessor processor) {
this.processor = processor;
}
public void handleIOException(IOException iox) {}
public void handleConnect(Socket socket) throws IOException {
processor.processSocket(socket, "GIV");
}
public void shutdown() {
}
}
/** Shutdownable that does nothing, because it's not possible for it to be shutdown. */
private static class NullMultiShutdownable implements MultiShutdownable {
public void shutdown() {
}
public void addShutdownable(Shutdownable newCancel) {
}
/** Returns true iff cancelled. */
public boolean isCancelled() {
return false;
}
}
/**
* Returns true if <code>address</code> is a {@link FirewalledAddress}
* the network manager has a valid local address and one of the following
* is true:
* <pre>
* 1) this peer can accept incoming connections
* 2) this peer can do firewalled transfers and the {@link FirewalledAddress}
* also supports firewalled transfers
* </pre>
*/
@Override
public boolean canConnect(Address address) {
int fwtVersion = getFWTVersion(address);
if (fwtVersion == -1) {
LOG.debugf("cannot connect: remote address {0} cannot do fwt", address);
return false;
}
if (hasValidLocalAddress()) {
if (networkManager.acceptedIncomingConnection()) {
LOG.debug("can connect: local address accepted incoming connection");
return true;
}
if (networkManager.canDoFWT() && fwtVersion > 0 && NetworkUtils.isValidIpPort(getPublicAddress(address))) {
LOG.debug("can connect: local and remote address can do fwt");
return true;
}
LOG.debugf("can not connect: have not accepted incoming connection and can not do FWT or invalid address: {0}", address);
} else {
LOG.debug(" can not connect: no local valid address");
}
LOG.debugf("cannot connect to {0}, local fwt {1}", address, networkManager.canDoFWT());
return false;
}
/**
* Returns the fwt version supported by address.
* Returns:<pre>
* 0 - if firewalled address or push endpoint but not fwt capability
* > 0 - if firewalled address or push endpoint and fwt capability
* -1 if not a valid address;
* </pre>
*/
private static int getFWTVersion(Address address) {
if (address instanceof FirewalledAddress) {
FirewalledAddress firewalledAddress = (FirewalledAddress)address;
if (NetworkUtils.isValidIpPort(firewalledAddress.getPrivateAddress())) {
return firewalledAddress.getFwtVersion();
} else {
LOG.debugf("inconsistent firewalled address: {0}", firewalledAddress);
throw new IllegalArgumentException("inconsistent firewalled address: " + firewalledAddress);
// return -1;
}
} else if (address instanceof PushEndpoint) {
return ((PushEndpoint)address).getFWTVersion();
} else {
return -1;
}
}
/**
* Adapts {@link PushedSocketHandler} and {@link MultiShutdownable} to {@link ConnectObserver}.
*/
private class PushedSocketHandlerAdapter implements PushedSocketHandler, MultiShutdownable {
private final RemoteFileDesc rfd;
private final ConnectObserver observer;
/**
* Keeps state whether connect has already succeeded or failed to make sure
* the observer is not notified several times.
*/
private final AtomicBoolean acceptedOrFailed = new AtomicBoolean(false);
public PushedSocketHandlerAdapter(RemoteFileDesc rfd, ConnectObserver observer) {
this.rfd = rfd;
this.observer = observer;
}
@Override
public boolean acceptPushedSocket(String file, int index, byte[] clientGUID, Socket socket) {
if (Arrays.equals(rfd.getClientGUID(), clientGUID)) {
pushHandlers.remove(this);
IpPort publicAddress = getPublicAddress(rfd.getAddress());
// this ensures that no malicious push proxy pretends to be the connecting client
if (NetworkUtils.isValidIpPort(publicAddress) && !publicAddress.getInetAddress().equals(socket.getInetAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("received socket from unexpected location, expected: " + publicAddress.getInetAddress() + ", actual: " + socket.getInetAddress());
return false;
}
}
if (acceptedOrFailed.compareAndSet(false, true)) {
try {
observer.handleConnect(socket);
} catch (IOException e) {
IOUtils.close(socket);
}
return true;
}
// return false if not handled for whatever reason, maybe there
// is another handler, or PushDownloadManager just closes it
return false;
}
return false;
}
/**
* Notifies observer that push has timed out unless it has already
* succeeded.
*/
public void handleTimeout() {
if (acceptedOrFailed.compareAndSet(false, true)) {
observer.handleIOException(new ConnectException("push timed out"));
}
}
@Override
public void addShutdownable(Shutdownable shutdowner) {
// we don't shutdown from the caller side, so we don't need to
// notify
}
@Override
public void shutdown() {
if (acceptedOrFailed.compareAndSet(false, true)) {
observer.handleIOException(new ConnectException("shut down"));
}
}
@Override
public boolean isCancelled() {
return false;
}
}
}