package com.limegroup.gnutella; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.udpconnect.UDPConnection; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.ManagedThread; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.Sockets; import com.limegroup.gnutella.util.ThreadFactory; /** * Handles all stuff necessary for browsing of networks hosts. * Has a instance component, one per browse host, and a static Map of instances * that is used to coordinate between replies to PushRequests. */ public class BrowseHostHandler { private static final Log LOG = LogFactory.getLog(BrowseHostHandler.class); /** * Various internal states for Browse-Hosting. */ private static final int NOT_STARTED = -1; private static final int STARTED = 0; private static final int DIRECTLY_CONNECTING = 1; private static final int PUSHING = 2; private static final int EXCHANGING = 3; private static final int FINISHED = 4; private static final int DIRECT_CONNECT_TIME = 10000; // 10 seconds. private static final long EXPIRE_TIME = 15000; // 15 seconds private static final int SPECIAL_INDEX = 0; /** Map from serventID to BrowseHostHandler instance. */ private static Map _pushedHosts = new HashMap(); /** The ActivityCallBack instance. Used for talking to GUI. */ private ActivityCallback _callback = null; /** The GUID to be used for incoming QRs from the Browse Request. */ private GUID _guid = null; /** The GUID of the servent to send a Push to. May be null if no push is * needed. */ private GUID _serventID = null; /** * The total length of the http-reply. */ private volatile long _replyLength = 0; /** * The current length of the reply. */ private volatile long _currentLength = 0; /** * The current state of this BH. */ private volatile int _state = NOT_STARTED; /** * The time this state started. */ private volatile long _stateStarted = 0; static { Expirer expirer = new Expirer(); RouterService.schedule(expirer, 0, 5000);// every 5 seconds } /** * @param callback A instance of a ActivityCallback, so I can notify it of * incoming QReps... * @param router A instance of a MessageRouter, so I can route messages if * needs be. * @param guid The GUID you have associated on the front end with the * results of this Browse Host request. * @param serventID May be null, non-null if I need to push */ public BrowseHostHandler(ActivityCallback callback, GUID guid, GUID serventID) { _callback = callback; _guid = guid; _serventID = serventID; } /** * Browses the files on the specified host and port. * * @param host The IP of the host you want to browse. * @param port The port of the host you want to browse. * @param proxies the <tt>Set</tt> of push proxies to try * @param canDoFWTransfer Whether or not this guy can do a firewall * transfer. */ public void browseHost(String host, int port, Set proxies, boolean canDoFWTransfer) { if(!NetworkUtils.isValidPort(port) || !NetworkUtils.isValidAddress(host)) { failed(); return; } LOG.trace("starting browse protocol."); setState(STARTED); // flow of operation: // 1. check if you need to push. // a. if so, just send a Push out. // b. if not, try direct connect. If it doesn't work, send a push. int shouldPush = needsPush(host); LOG.trace("push needed? " + shouldPush); boolean shouldTryPush = false; switch (shouldPush) { case 0: // false try { // simply try connecting and getting results.... setState(DIRECTLY_CONNECTING); Socket socket = Sockets.connect(host, port, DIRECT_CONNECT_TIME); LOG.trace("direct connect successful"); browseExchange(socket); } catch (IOException ioe) { // try pushing for fun.... (if we have the guid of the servent) shouldTryPush = true; } if (!shouldTryPush) break; case 1: // true // if we're trying to push & we don't have a servent guid, it fails if ( _serventID == null ) { failed(); } else { RemoteFileDesc fakeRFD = new RemoteFileDesc(host, port, SPECIAL_INDEX, "fake", 0, _serventID.bytes(), 0, false, 0, false, null, null,false,true,"",0l, proxies, -1, canDoFWTransfer ? UDPConnection.VERSION : 0); // register with the map so i get notified about a response to my // Push. synchronized (_pushedHosts) { _pushedHosts.put(_serventID, new PushRequestDetails(this)); } LOG.trace("trying push."); setState(PUSHING); // send the Push after registering in case you get a response // really quickly. reuse code in DM cuz that works well RouterService.getDownloadManager().sendPush(fakeRFD); } break; } } /** * Returns the current percentage complete of the state * of the browse host. */ public double getPercentComplete(long currentTime) { long elapsed; switch(_state) { case NOT_STARTED: return 0d; case STARTED: return 0d; case DIRECTLY_CONNECTING: // return how long it'll take to connect. elapsed = currentTime - _stateStarted; return (double) elapsed / DIRECT_CONNECT_TIME; case PUSHING: // return how long it'll take to push. elapsed = currentTime - _stateStarted; return (double) elapsed / EXPIRE_TIME; case EXCHANGING: // return how long it'll take to finish reading, // or stay at .5 if we dunno the length. if( _replyLength > 0 ) return (double)_currentLength / _replyLength; else return 0.5; case FINISHED: return 1.0; default: throw new IllegalStateException("invalid state"); } } /** * Sets the state and state-time. */ private void setState(int state) { _state = state; _stateStarted = System.currentTimeMillis(); } /** * Indicates that this browse host has failed. */ private void failed() { setState(FINISHED); _callback.browseHostFailed(_guid); } private void browseExchange(Socket socket) throws IOException { try { browseExchangeInternal(socket); }finally { try{socket.close();}catch(IOException ignored){} setState(FINISHED); } } private void browseExchangeInternal(Socket socket) throws IOException { //when/if we start reusing connections, remove this timeout socket.setSoTimeout(5000); LOG.trace("BHH.browseExchange(): entered."); setState(EXCHANGING); // first write the request... final String LF = "\r\n"; String str = null; OutputStream oStream = socket.getOutputStream(); LOG.trace("BHH.browseExchange(): got output stream."); // ask for the browse results.. str = "GET / HTTP/1.1" + LF; oStream.write(str.getBytes()); str = "Host: " + NetworkUtils.ip2string(RouterService.getAddress()) + ":" + RouterService.getPort() + LF; oStream.write(str.getBytes()); str = "User-Agent: " + CommonUtils.getVendor() + LF; oStream.write(str.getBytes()); str = "Accept: " + Constants.QUERYREPLY_MIME_TYPE + LF; oStream.write(str.getBytes()); str = "Content-Length: 0" + LF; oStream.write(str.getBytes()); str = "Connection: close" + LF; oStream.write(str.getBytes()); str = LF; oStream.write(str.getBytes()); oStream.flush(); LOG.trace("BHH.browseExchange(): wrote request A-OK."); // get the results... InputStream in = socket.getInputStream(); LOG.trace("BHH.browseExchange(): got input stream."); // first check the HTTP code, encoding, etc... ByteReader br = new ByteReader(in); LOG.trace("BHH.browseExchange(): trying to get HTTP code...."); int code = parseHTTPCode(br.readLine()); if ((code < 200) || (code >= 300)) throw new IOException(); if(LOG.isDebugEnabled()) LOG.debug("BHH.browseExchange(): HTTP Response is " + code); // now confirm the content-type, the encoding, etc... boolean readingHTTP = true; String currLine = null; while (readingHTTP) { currLine = br.readLine(); if(LOG.isDebugEnabled()) LOG.debug("BHH.browseExchange(): currLine = " + currLine); if ((currLine == null) || currLine.equals("")) { // start processing queries... readingHTTP = false; } else if (indexOfIgnoreCase(currLine, "Content-Type") > -1) { // make sure it is QRs.... if (indexOfIgnoreCase(currLine, Constants.QUERYREPLY_MIME_TYPE) < 0) throw new IOException(); } else if (indexOfIgnoreCase(currLine, "Content-Encoding") > -1) { throw new IOException(); // decompress currently not supported } else if (markContentLength(currLine)) ; // do nothing special } LOG.debug("BHH.browseExchange(): read HTTP seemingly OK."); in = new BufferedInputStream(in); // ok, everything checks out, proceed and read QRs... Message m = null; while(true) { try { m = null; LOG.debug("reading message"); m = Message.read(in); LOG.debug("read message "+m); } catch (BadPacketException bpe) {LOG.debug(bpe);} catch (IOException expected){} // either timeout, or the remote closed. if(m == null) return; else { if(m instanceof QueryReply) { _currentLength += m.getTotalLength(); if(LOG.isTraceEnabled()) LOG.trace("BHH.browseExchange(): read QR:" + m); QueryReply reply = (QueryReply)m; reply.setGUID(_guid); reply.setBrowseHostReply(true); ForMeReplyHandler.instance().handleQueryReply(reply, null); } } } } /** * Reads and marks the content-length for this line, if exists. */ private boolean markContentLength(final String line) { int idx = indexOfIgnoreCase(line, "Content-Length:"); if( idx < 0 ) return false; // get the string after the ':' String length = line.substring("Content-Length:".length()).trim(); try { _replyLength = Long.parseLong(length); } catch(NumberFormatException ignored) { // ignore. } return true; } /** Returns 1 iff rfd should be attempted by push download, either * because it is a private address or was unreachable in the past. * Returns 0 otherwise.... */ private static int needsPush(String host) { //Return true if rfd is private or unreachable if (ConnectionSettings.LOCAL_IS_PRIVATE.getValue() && NetworkUtils.isPrivateAddress(host)) return 1; else return 0; } /** * a helper method to compare two strings * ignoring their case. */ private int indexOfIgnoreCase(String str, String section) { // convert both strings to lower case String aaa = str.toLowerCase(); String bbb = section.toLowerCase(); // then look for the index... return aaa.indexOf(bbb); } /** * Returns the HTTP response code from the given string, throwing * an exception if it couldn't be parsed. * * @param str an HTTP response string, e.g., "HTTP 200 OK \r\n" * @exception IOException a problem occurred */ private static int parseHTTPCode(String str) throws IOException { if (str == null) return -1; // hopefully this won't happen, but if so just error... StringTokenizer tokenizer = new StringTokenizer(str, " "); String token; // just a safety if (! tokenizer.hasMoreTokens() ) throw new IOException(); token = tokenizer.nextToken(); // the first token should contain HTTP if (token.toUpperCase().indexOf("HTTP") < 0 ) throw new IOException(); // the next token should be a number // just a safety if (! tokenizer.hasMoreTokens() ) throw new IOException(); token = tokenizer.nextToken(); String num = token.trim(); try { if(LOG.isDebugEnabled()) LOG.debug("BHH.parseHTTPCode(): returning " + num); return java.lang.Integer.parseInt(num); } catch (NumberFormatException e) { throw new IOException(); } } /** @return true if the Push was handled by me. */ public static boolean handlePush(int index, GUID serventID, final Socket socket) { boolean retVal = false; LOG.trace("BHH.handlePush(): entered."); // if (index == SPECIAL_INDEX) // ; // you'd hope, but not necessary... PushRequestDetails prd = null; synchronized (_pushedHosts) { prd = (PushRequestDetails) _pushedHosts.remove(serventID); } if (prd != null) { final PushRequestDetails finalPRD = prd; ThreadFactory.startThread(new Runnable() { public void run() { try { finalPRD.bhh.browseExchange(socket); } catch (IOException ohWell) { finalPRD.bhh.failed(); } } }, "BrowseHost"); retVal = true; } else LOG.debug("BHH.handlePush(): no matching BHH."); LOG.trace("BHH.handlePush(): returning."); return retVal; } /** Can be run to invalidate pushes that we are waiting for.... */ private static class Expirer implements Runnable { public void run() { try { Iterator keys = null; Set toRemove = new HashSet(); synchronized (_pushedHosts) { keys = _pushedHosts.keySet().iterator(); while (keys.hasNext()) { Object currKey = keys.next(); PushRequestDetails currPRD = null; currPRD = (PushRequestDetails) _pushedHosts.get(currKey); if ((currPRD != null) && (currPRD.isExpired())) { LOG.debug("Expirer.run(): expiring a badboy."); toRemove.add(currKey); currPRD.bhh.failed(); } } // done iterating through _pushedHosts, remove the keys now... keys = toRemove.iterator(); while (keys.hasNext()) _pushedHosts.remove(keys.next()); } } catch(Throwable t) { ErrorService.error(t); } } } private static class PushRequestDetails { private BrowseHostHandler bhh; private long timeStamp; public PushRequestDetails(BrowseHostHandler bhh) { timeStamp = System.currentTimeMillis(); this.bhh = bhh; } public boolean isExpired() { return ((System.currentTimeMillis() - timeStamp) > EXPIRE_TIME); } } }