package com.limegroup.gnutella;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.Connectable;
import org.limewire.io.GUID;
import org.limewire.io.NetworkUtils;
import org.limewire.listener.EventListener;
import org.limewire.listener.ListenerSupport;
import org.limewire.net.ConnectBackRequest;
import org.limewire.net.ConnectBackRequestedEvent;
import org.limewire.net.SocketsManager;
import org.limewire.net.SocketsManager.ConnectType;
import org.limewire.nio.NBSocket;
import org.limewire.nio.channel.ChannelWriter;
import org.limewire.nio.channel.InterestWritableByteChannel;
import org.limewire.nio.channel.NIOMultiplexor;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.rudp.UDPSelectorProvider;
import org.limewire.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.limegroup.gnutella.http.HTTPConnectionData;
/**
* Manages state for push upload requests.
*/
@EagerSingleton
public final class PushManager implements EventListener<ConnectBackRequestedEvent> {
private static final Log LOG = LogFactory.getLog(PushManager.class);
/**
* The timeout for the connect time while establishing the socket. Set to
* the same value as NORMAL_CONNECT_TIME is ManagedDownloader.
*/
private static final int CONNECT_TIMEOUT = 10000;//10 secs
private final Provider<SocketsManager> socketsManager;
private final Provider<HTTPAcceptor> httpAcceptor;
private final Provider<UDPSelectorProvider> udpSelectorProvider;
private final Provider<NetworkManager> networkManager;
/**
* @param socketsManager
* @param httpAcceptor
*/
@Inject
public PushManager(Provider<SocketsManager> socketsManager,
Provider<HTTPAcceptor> httpAcceptor,
Provider<UDPSelectorProvider> udpSelectorProvider,
Provider<NetworkManager> networkManager) {
this.socketsManager = socketsManager;
this.httpAcceptor = httpAcceptor;
this.udpSelectorProvider = udpSelectorProvider;
this.networkManager = networkManager;
}
@Inject
void register(ListenerSupport<ConnectBackRequestedEvent> connectBackRequestedEventListenerSupport) {
// listener is leaked, but both are singleton scope, so it's fine
connectBackRequestedEventListenerSupport.addListener(this);
}
/**
* Accepts a new push upload asynchronously.
* <p>
* Essentially, this is a reverse-Acceptor.
* <p>
* No file and index are needed since the GET/HEAD will include that
* information. Just put in our first file and filename to create a
* well-formed.
* @param address the connectable that will be connected to
* @param guid the unique identifying client guid of the push-connecting client
* @param lan whether or not this is a request over a local network (
* (force the UploadManager to accept this request when it comes back)
* @param isFWTransfer whether or not to use a UDP pipe to service this
* upload.
*/
public void acceptPushUpload(Connectable address,
final GUID guid,
final boolean lan,
final boolean isFWTransfer) {
if (LOG.isDebugEnabled())
LOG.debug("Accepting Push Upload from host:" + address + " FW:" + isFWTransfer);
if( address == null )
throw new NullPointerException("null host");
if( !NetworkUtils.isValidIpPort(address) )
throw new IllegalArgumentException("invalid ip port: " + address);
if( guid == null )
throw new NullPointerException("null guid");
// We used to have code here that tested if the guy we are pushing to is
// 1) hammering us, or 2) is actually firewalled. 1) is done above us
// now, and 2) isn't as much an issue with the advent of connectback
PushData data = new PushData(address, guid, lan);
// If the transfer is to be done using FW-FW, then immediately start a new thread
// which will connect using FWT. Otherwise, do a non-blocking connect and have
// the observer spawn the thread only if it succesfully connected.
if(isFWTransfer) {
if(LOG.isDebugEnabled())
LOG.debug("Adding push observer FW-FW to host: " + address);
// TODO: should FW-FW connections also use TLS?
NBSocket socket = udpSelectorProvider.get().openAcceptorSocketChannel().socket();
socket.connect(address.getInetSocketAddress(), CONNECT_TIMEOUT*2, new PushConnectObserver(data, true, httpAcceptor.get()));
} else {
if (LOG.isDebugEnabled())
LOG.debug("Adding push observer to host: " + address);
try {
ConnectType type = address.isTLSCapable() && networkManager.get().isOutgoingTLSEnabled() ? ConnectType.TLS : ConnectType.PLAIN;
socketsManager.get().connect(address.getInetSocketAddress(), CONNECT_TIMEOUT, new PushConnectObserver(data, false, httpAcceptor.get()), type);
} catch(IOException iox) {
}
}
}
/** A simple collection of Push information */
private static class PushData {
private final Connectable address;
private final GUID guid;
private final boolean lan;
PushData(Connectable address, GUID guid, boolean lan) {
this.address = address;
this.guid = guid;
this.lan = lan;
}
public boolean isLan() {
return lan;
}
public GUID getGuid() {
return guid;
}
public Connectable getAddress() {
return address;
}
}
/** Non-blocking observer for connect events related to pushing. */
private static class PushConnectObserver implements ConnectObserver {
private final PushData data;
private final boolean fwt;
private final HTTPAcceptor httpAcceptor;
PushConnectObserver(PushData data, boolean fwt, HTTPAcceptor httpAcceptor) {
this.data = data;
this.fwt = fwt;
this.httpAcceptor = httpAcceptor;
}
public void handleIOException(IOException iox) {}
/** Increments the PUSH_FAILED stat and does nothing else. */
public void shutdown() {
if(LOG.isDebugEnabled())
LOG.debug("Push (fwt: " + fwt + ") connect to: " + data.getAddress() + " failed");
}
/** Starts a new thread that'll do the pushing. */
public void handleConnect(Socket socket) throws IOException {
if(LOG.isDebugEnabled())
LOG.debug("Push (fwt: " + fwt + ") connect to: " + data.getAddress() + " succeeded");
((NIOMultiplexor) socket).setWriteObserver(new GivLineWriter(socket, data, fwt, httpAcceptor));
}
}
/** Non-blocking writer that writes out the give line after a socket connection has been established. */
private static class GivLineWriter implements ChannelWriter {
private InterestWritableByteChannel channel;
private final ByteBuffer buffer;
private final Socket socket;
private HTTPConnectionData data;
private HTTPAcceptor httpAcceptor;
public GivLineWriter(Socket socket, PushData data, boolean fwTransfer,
HTTPAcceptor httpAcceptor) throws IOException {
this.socket = socket;
this.data = new HTTPConnectionData();
this.data.setPush(true);
this.data.setLocal(data.isLan());
this.data.setFirewalled(fwTransfer);
this.httpAcceptor = httpAcceptor;
socket.setSoTimeout(30 * 1000);
String giv = "GIV 0:" + data.getGuid() + "/file\n\n";
this.buffer = ByteBuffer.wrap(StringUtils.toAsciiBytes(giv));
}
public boolean handleWrite() throws IOException {
if (!buffer.hasRemaining()) {
return false;
}
while (buffer.hasRemaining()) {
int written = channel.write(buffer);
if (written == 0) {
return true;
}
}
httpAcceptor.acceptConnection(socket, data);
return false;
}
public void handleIOException(IOException iox) {
throw new RuntimeException();
}
public void shutdown() {
// ignore
}
public InterestWritableByteChannel getWriteChannel() {
return channel;
}
public void setWriteChannel(InterestWritableByteChannel newChannel) {
this.channel = newChannel;
if (newChannel != null) {
newChannel.interestWrite(this, true);
}
}
}
@Override
public void handleEvent(ConnectBackRequestedEvent event) {
ConnectBackRequest request = event.getData();
// can assume false for lan, since same NAT resolver would have spotted that and opened a direct connection
acceptPushUpload(request.getAddress(), request.getClientGuid(), false, request.getSupportedFWTVersion() > 0);
}
}