package com.limegroup.gnutella; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.http.HTTPRequestMethod; import com.limegroup.gnutella.io.ConnectObserver; import com.limegroup.gnutella.statistics.UploadStat; import com.limegroup.gnutella.udpconnect.UDPConnection; import com.limegroup.gnutella.util.IOUtils; import com.limegroup.gnutella.util.ManagedThread; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.Sockets; import com.limegroup.gnutella.util.ThreadFactory; /** * Manages state for push upload requests. */ public final class PushManager { 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 /** * Accepts a new push upload. * NON-BLOCKING: creates a new thread to transfer the file. * <p> * The thread connects to the other side, waits for a GET/HEAD, * and delegates to the UploaderManager.acceptUpload method with the * socket it created. * 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 host the ip address of the host to upload to * @param port the port over which the transfer will occur * @param guid the unique identifying client guid of the uploading client * @param forceAllow whether or not to force the UploadManager to send * accept this request when it comes back. * @param isFWTransfer whether or not to use a UDP pipe to service this * upload. */ public void acceptPushUpload(final String host, final int port, final String guid, final boolean forceAllow, final boolean isFWTransfer) { if (LOG.isDebugEnabled()) LOG.debug("Accepting Push Upload from ip:" + host + " port:" + port + " FW:" + isFWTransfer); if( host == null ) throw new NullPointerException("null host"); if( !NetworkUtils.isValidPort(port) ) throw new IllegalArgumentException("invalid port: " + port); if( guid == null ) throw new NullPointerException("null guid"); FileManager fm = RouterService.getFileManager(); // TODO: why is this check here? it's a tiny optimization, // but could potentially kill any sharing of files that aren't // counted in the library. if (fm.getNumFiles() < 1 && fm.getNumIncompleteFiles() < 1) return; // 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(host, port, guid, forceAllow); // 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) { startPushRunner(data, null); } else { if (LOG.isDebugEnabled()) LOG.debug("Adding push observer to host: " + host + ":" + port); try { Sockets.connect(host, port, CONNECT_TIMEOUT, new PushObserver(data)); } catch(IOException iox) { UploadStat.PUSH_FAILED.incrementStat(); } } } /** * Starts a thread that'll do the pushing using the given PushData & Socket. * @param data All the data about the push. * @param socket The possibly null socket. */ private static void startPushRunner(PushData data, Socket socket) { ThreadFactory.startThread(new Pusher(data, socket), "PushUploadThread"); } /** A simple collection of Push information */ private static class PushData { private final String host; private final int port; private final String guid; private final boolean forceAllow; PushData(String host, int port, String guid, boolean forceAllow) { this.host = host; this.port = port; this.guid = guid; this.forceAllow = forceAllow; } public boolean isForceAllow() { return forceAllow; } public String getGuid() { return guid; } public String getHost() { return host; } public int getPort() { return port; } } /** Non-blocking observer for connect events related to pushing. */ private static class PushObserver implements ConnectObserver { private final PushData data; PushObserver(PushData data) { this.data = data; } public void handleIOException(IOException iox) {} /** Increments the PUSH_FAILED stat and does nothing else. */ public void shutdown() { if(LOG.isDebugEnabled()) LOG.debug("Push connect to: " + data.getHost() + ":" + data.getPort() + " failed"); UploadStat.PUSH_FAILED.incrementStat(); } /** Starts a new thread that'll do the pushing. */ public void handleConnect(Socket socket) throws IOException { if(LOG.isDebugEnabled()) LOG.debug("Push connect to: " + data.getHost() + ":" + data.getPort() + " succeeded"); startPushRunner(data, socket); } } /** A runnable that starts a push transfer. */ private static class Pusher implements Runnable { PushData data; private Socket socket; private boolean fwTransfer; Pusher(PushData data, Socket socket) { this.data = data; this.socket = socket; } public void run() { try { if (socket == null) { if (LOG.isDebugEnabled()) LOG.debug("Creating UDP Connection to " + data.getHost() + ":" + data.getPort()); fwTransfer = true; socket = new UDPConnection(data.getHost(), data.getPort()); } OutputStream ostream = socket.getOutputStream(); String giv = "GIV 0:" + data.getGuid() + "/file\n\n"; ostream.write(giv.getBytes()); ostream.flush(); // try to read a GET or HEAD for only 30 seconds. socket.setSoTimeout(30 * 1000); // read GET or HEAD and delegate appropriately. String word = IOUtils.readWord(socket.getInputStream(), 4); if (fwTransfer) UploadStat.FW_FW_SUCCESS.incrementStat(); if (word.equals("GET")) { UploadStat.PUSHED_GET.incrementStat(); RouterService.getUploadManager().acceptUpload(HTTPRequestMethod.GET, socket, data.isForceAllow()); } else if (word.equals("HEAD")) { UploadStat.PUSHED_HEAD.incrementStat(); RouterService.getUploadManager().acceptUpload(HTTPRequestMethod.HEAD, socket, data.isForceAllow()); } else { UploadStat.PUSHED_UNKNOWN.incrementStat(); throw new IOException(); } } catch (IOException ioe) { if(LOG.isDebugEnabled()) LOG.debug("Failed push connect/transfer to " + data.getHost() + ":" + data.getPort() + ", fwt: " + fwTransfer); if (fwTransfer) UploadStat.FW_FW_FAILURE.incrementStat(); UploadStat.PUSH_FAILED.incrementStat(); } finally { IOUtils.close(socket); } } } }