package com.limegroup.gnutella.messages.vendor; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.collection.BitNumbers; import org.limewire.collection.IntervalSet; import org.limewire.collection.MultiRRIterator; import org.limewire.core.settings.UploadSettings; import org.limewire.io.Connectable; import org.limewire.io.CountingOutputStream; import org.limewire.io.GGEP; import org.limewire.io.GUID; import org.limewire.io.IpPort; import org.limewire.service.ErrorService; import org.limewire.util.ByteUtils; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.limegroup.gnutella.DownloadManager; import com.limegroup.gnutella.NetworkManager; import com.limegroup.gnutella.PushEndpointFactory; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.UploadManager; import com.limegroup.gnutella.altlocs.AltLocManager; import com.limegroup.gnutella.altlocs.AlternateLocation; import com.limegroup.gnutella.altlocs.AlternateLocationCollection; import com.limegroup.gnutella.altlocs.DirectAltLoc; import com.limegroup.gnutella.altlocs.PushAltLoc; import com.limegroup.gnutella.library.FileDesc; import com.limegroup.gnutella.library.FileView; import com.limegroup.gnutella.library.GnutellaFiles; import com.limegroup.gnutella.library.IncompleteFileDesc; import com.limegroup.gnutella.library.IncompleteFiles; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message.Network; @Singleton public class HeadPongFactoryImpl implements HeadPongFactory { private static final Log LOG = LogFactory.getLog(HeadPongFactoryImpl.class); private final NetworkManager networkManager; private final Provider<UploadManager> uploadManager; private final FileView gnutellaFileView; private final FileView incompleteFileView; private final Provider<AltLocManager> altLocManager; private final PushEndpointFactory pushEndpointFactory; /** The real packet size. */ public static final int DEFAULT_PACKET_SIZE = 1380; /** The packet size used by this class -- non-final for testing. */ // TODO: Should either be a parameter in the constructor, or changed by a setter private static /*final*/ int PACKET_SIZE = DEFAULT_PACKET_SIZE; private final Provider<DownloadManager> downloadManager; @Inject public HeadPongFactoryImpl(NetworkManager networkManager, Provider<UploadManager> uploadManager, Provider<AltLocManager> altLocManager, PushEndpointFactory pushEndpointFactory, Provider<DownloadManager> downloadManager, @GnutellaFiles FileView gnutellaFileView, @IncompleteFiles FileView incompleteFileView) { this.networkManager = networkManager; this.uploadManager = uploadManager; this.altLocManager = altLocManager; this.pushEndpointFactory = pushEndpointFactory; this.downloadManager = downloadManager; this.gnutellaFileView = gnutellaFileView; this.incompleteFileView = incompleteFileView; } /* (non-Javadoc) * @see com.limegroup.gnutella.messages.vendor.HeadPongFactory#createFromNetwork(byte[], byte, byte, int, byte[]) */ public HeadPong createFromNetwork(byte[] guid, byte ttl, byte hops, int version, byte[] payload, Network network) throws BadPacketException { return new HeadPongImpl(guid, ttl, hops, version, payload, network, pushEndpointFactory); } /* (non-Javadoc) * @see com.limegroup.gnutella.messages.vendor.HeadPongFactory#create(com.limegroup.gnutella.messages.vendor.HeadPongRequestor) */ public HeadPong create(HeadPongRequestor ping) { return new HeadPongImpl(new GUID(ping.getGUID()), versionFor(ping), derivePayload(ping)); } /** Returns the byte[] of the written GGEP. */ private byte[] writeGGEP(GGEP ggep) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ggep.write(out); } catch(IOException iox) { ErrorService.error(iox); } return out.toByteArray(); } /** Adds direct locations, if possible. */ private boolean addLocations(HeadPongRequestor ping, URN urn, OutputStream out, AtomicReference<BitNumbers> tlsIndexes, int written, boolean includeSize) { //now add any non-firewalled altlocs in case they were requested. if (ping.requestsAltlocs()) { AlternateLocationCollection<DirectAltLoc> col = altLocManager.get().getDirect(urn); synchronized(col) { try { return !writeLocs(out, col.iterator(), tlsIndexes, written, includeSize); } catch(IOException impossible) { ErrorService.error(impossible); } } } return false; } /** Adds push locations, if possible. */ private boolean addPushLocations(HeadPongRequestor ping, URN urn, OutputStream out, boolean includeTLS, int written, boolean includeSize) { if(!ping.requestsPushLocs()) return true; try { boolean FWTOnly = ping.requestsFWTOnlyPushLocs(); if (FWTOnly) { AlternateLocationCollection<PushAltLoc> push = altLocManager.get().getPushFWT(urn); synchronized(push) { return !writePushLocs(out, push.iterator(), includeTLS, written, includeSize); } } else { AlternateLocationCollection<PushAltLoc> push = altLocManager.get().getPushNoFWT(urn); AlternateLocationCollection<PushAltLoc> fwt = altLocManager.get().getPushFWT(urn); synchronized(push) { synchronized(fwt) { return !writePushLocs(out, new MultiRRIterator<PushAltLoc>(push.iterator(), fwt.iterator()), includeTLS, written, includeSize); } } } } catch(IOException impossible) { ErrorService.error(impossible); return false; } } /** Calculates the queue status. */ private byte calculateQueueStatus() { int queueSize = uploadManager.get().getNumQueuedUploads(); if(queueSize >= UploadSettings.UPLOAD_QUEUE_SIZE.getValue()) { return HeadPong.BUSY; } else if(queueSize > 0) { return (byte) Math.min(queueSize, 127); // 127 == HeadPong.BUSY } else { // Negative queue status means free slots queueSize = uploadManager.get().uploadsInProgress() - UploadSettings.HARD_MAX_UPLOADS.getValue(); return (byte) Math.max(Math.min(queueSize, 127), -128); } } /** Calculates the code that should be returned, based on the FileDesc. */ private byte calculateCode(FileDesc fd) { byte code = 0; if(!networkManager.acceptedIncomingConnection()) { code = HeadPong.FIREWALLED; } if(fd instanceof IncompleteFileDesc) { code |= HeadPong.PARTIAL_FILE; if (downloadManager.get().isActivelyDownloading(fd.getSHA1Urn())) { code |= HeadPong.DOWNLOADING; } } else { code |= HeadPong.COMPLETE_FILE; } return code; } /** Constructs the payload in GGEP format. */ private byte[] constructGGEPPayload(HeadPongRequestor ping) { GGEP ggep = new GGEP(); URN urn = ping.getUrn(); FileDesc desc = gnutellaFileView.getFileDesc(urn); if(desc == null) { desc = incompleteFileView.getFileDesc(urn); } // Easy case: no file, add code & exit if(desc == null) { ggep.put(HeadPong.CODE, HeadPong.FILE_NOT_FOUND); return writeGGEP(ggep); } // OK, we have the file, now what! int size = 1; // begin with 1 because of GGEP magic // If we're not firewalled and support TLS, // spread word about our TLS status. if(networkManager.acceptedIncomingConnection() && networkManager.isIncomingTLSEnabled() ) { ggep.put(HeadPong.FEATURES, HeadPong.TLS_CAPABLE); size += 4; } byte code = calculateCode(desc); ggep.put(HeadPong.CODE, code); size += ggep.getHeaderOverhead(HeadPong.CODE); ggep.put(HeadPong.VENDOR, VendorMessage.F_LIME_VENDOR_ID); size += ggep.getHeaderOverhead(HeadPong.VENDOR); ggep.put(HeadPong.QUEUE, calculateQueueStatus()); size += ggep.getHeaderOverhead(HeadPong.QUEUE); // NOTE: All insertion checks assume that the header is going to take up // the maximum amount of bytes possible for a GGEP header + overhead. if((code & HeadPong.PARTIAL_FILE) == HeadPong.PARTIAL_FILE && ping.requestsRanges()) { IntervalSet.ByteIntervals ranges = deriveRanges(desc); if(ranges.length() == 0) { // If we have no ranges available, change queue status to busy, // so that they come back and ask us later, when we may have // more ranges available. (but don't increment size, since that // was already done above.) ggep.put(HeadPong.QUEUE, HeadPong.BUSY); } else if(size + ranges.length() + 11 <= PACKET_SIZE) { //5 for "R" and 6 for "R5" if (ranges.ints.length > 0) { ggep.put(HeadPong.RANGES, ranges.ints); size += ggep.getHeaderOverhead(HeadPong.RANGES); } if (ranges.longs.length > 0) { ggep.put(HeadPong.RANGES5, ranges.longs); size += ggep.getHeaderOverhead(HeadPong.RANGES5); } } } ByteArrayOutputStream out = new ByteArrayOutputStream(); addPushLocations(ping, urn, out, true, size+5, false); if(out.size() > 0) { byte[] pushLocs = out.toByteArray(); ggep.put(HeadPong.PUSH_LOCS, pushLocs); size += ggep.getHeaderOverhead(HeadPong.PUSH_LOCS); } out.reset(); AtomicReference<BitNumbers> bnRef = new AtomicReference<BitNumbers>(); addLocations(ping, urn, out, bnRef, size+5, false); if(out.size() > 0) { byte[] altLocs = out.toByteArray(); ggep.put(HeadPong.LOCS, altLocs); size += ggep.getHeaderOverhead(HeadPong.LOCS); } // If it went over, we screwed up somewhere. assert size <= PACKET_SIZE : "size is too big "+size+" vs "+PACKET_SIZE; // Here we fudge a bit -- possibly going over PACKET_SIZE. BitNumbers bn = bnRef.get(); if(bn != null) { byte[] bnBytes = bn.toByteArray(); if(bnBytes.length > 0) { ggep.put(HeadPong.TLS_LOCS, bnBytes); size += ggep.getHeaderOverhead(HeadPong.TLS_LOCS); } } byte[] output = writeGGEP(ggep); assert output.length == size : "expected: " + size + ", was: " + output.length; return output; } /** Constructs the payload in binary format. */ private byte[] constructBinaryPayload(HeadPongRequestor ping) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); CountingOutputStream caos = new CountingOutputStream(baos); DataOutputStream daos = new DataOutputStream(caos); byte retCode=0; URN urn = ping.getUrn(); FileDesc desc = gnutellaFileView.getFileDesc(urn); if(desc == null) { desc = incompleteFileView.getFileDesc(urn); } boolean didNotSendAltLocs=false; boolean didNotSendPushAltLocs = false; boolean didNotSendRanges = false; try { byte features = ping.getFeatures(); features &= ~HeadPing.GGEP_PING; daos.write(features); if (LOG.isDebugEnabled()) LOG.debug("writing features "+features); //if we don't have the file or its too large... if (desc == null || desc.getFileSize() > Integer.MAX_VALUE) { LOG.debug("we do not have the file"); daos.write(HeadPong.FILE_NOT_FOUND); return baos.toByteArray(); } retCode = calculateCode(desc); daos.write(retCode); if(LOG.isDebugEnabled()) LOG.debug("our return code is "+retCode); //write the vendor id daos.write(VendorMessage.F_LIME_VENDOR_ID); //write out the return code and the queue status daos.writeByte(calculateQueueStatus()); //if we sent partial file and the remote asked for ranges, send them if ((retCode & HeadPong.PARTIAL_FILE) == HeadPong.PARTIAL_FILE && ping.requestsRanges()) didNotSendRanges=!writeRanges(caos,desc); didNotSendPushAltLocs = addPushLocations(ping, urn, caos, false, caos.getAmountWritten(), true); didNotSendAltLocs = addLocations(ping, urn, caos, null, caos.getAmountWritten(), true); } catch(IOException impossible) { ErrorService.error(impossible); } //done! byte []ret = baos.toByteArray(); //if we did not add ranges or altlocs due to constraints, //update the flags now. if (didNotSendRanges){ LOG.debug("not sending ranges"); ret[0] = (byte) (ret[0] & ~HeadPing.INTERVALS); } if (didNotSendAltLocs){ LOG.debug("not sending altlocs"); ret[0] = (byte) (ret[0] & ~HeadPing.ALT_LOCS); } if (didNotSendPushAltLocs){ LOG.debug("not sending push altlocs"); ret[0] = (byte) (ret[0] & ~HeadPing.PUSH_ALTLOCS); } return ret; } /** * Constructs a byte[] that contains the payload of the HeadPong. * * @param ping the original UDP head ping to respond to */ private byte [] derivePayload(HeadPongRequestor ping) { if(!ping.isPongGGEPCapable()) { return constructBinaryPayload(ping); } else { return constructGGEPPayload(ping); } } /** Determines the version that will be used based on the requestor. */ private int versionFor(HeadPongRequestor ping) { if(!ping.isPongGGEPCapable()) return HeadPong.BINARY_VERSION; else return HeadPong.VERSION; } /** Returns the byte[] of the ranges. */ private final IntervalSet.ByteIntervals deriveRanges(FileDesc desc) { return ((IncompleteFileDesc)desc).getRangesAsByte(); } /** * Writes out alternate locations in binary form to the output stream. * This will only write as many locations as possible that can * fit in the PACKET_SIZE. If tlsIndexes is non-null, the reference * will be set to a BitNumbers whose size is the number of locations * that are attempted to write, with the corresponding bits set * if the location at the index is tls capable. * If includeSize is true, the written data will be prepended by the length * of the amount written. */ private final boolean writeLocs(OutputStream out, Iterator<DirectAltLoc> altlocs, AtomicReference<BitNumbers> tlsIndexes, int written, boolean includeSize) throws IOException { //do we have any altlocs? if (!altlocs.hasNext()) return false; //how many can we fit in the packet? int toSend = (PACKET_SIZE - (written + (includeSize ? 2 : 0)) ) / 6; if (toSend == 0) return false; if (LOG.isDebugEnabled()) LOG.debug("trying to add up to "+ toSend +" locs to pong"); BitNumbers bn = null; if(tlsIndexes != null) { bn = new BitNumbers(toSend); tlsIndexes.set(bn); } // optimization: do not duplicate byte[] if not needed ByteArrayOutputStream baos = includeSize ? new ByteArrayOutputStream() : (ByteArrayOutputStream)out; int sent = 0; long now = System.currentTimeMillis(); while(altlocs.hasNext() && sent < toSend) { DirectAltLoc loc = altlocs.next(); if (loc.canBeSent(AlternateLocation.MESH_PING)) { loc.send(now,AlternateLocation.MESH_PING); baos.write(loc.getHost().getInetAddress().getAddress()); ByteUtils.short2leb((short)loc.getHost().getPort(),baos); IpPort host = loc.getHost(); if(bn != null && host instanceof Connectable && ((Connectable)host).isTLSCapable()) bn.set(sent); sent++; } else if (!loc.canBeSentAny()) altlocs.remove(); } LOG.debug("adding altlocs"); if(includeSize) { ByteUtils.short2beb((short)baos.size(),out); baos.writeTo(out); } return true; } /** * Writes out PushEndpoints in binary form to the output stream. * This will only write as many push locations as possible that can * fit in the PACKET_SIZE. If includeTLS is true, this will include * a byte that describes which push proxies of each PushEndpoint * are capable of receiving TLS connections. * If includeSize is true, the written data will be prepended by the length * of the amount written. */ private final boolean writePushLocs(OutputStream out, Iterator<PushAltLoc> pushlocs, boolean includeTLS, int written, boolean includeSize) throws IOException { if (!pushlocs.hasNext()) return false; //push altlocs are bigger than normal altlocs, however we //don't know by how much. The size can be between //23 and 48 bytes. We assume its 47 if includeTLS is false, 48 otherwise. int available = (PACKET_SIZE - (written + (includeSize ? 2 : 0))) / (includeTLS ? 48 : 47); // if we don't have any space left, we can't send any pushlocs if (available == 0) return false; if (LOG.isDebugEnabled()) LOG.debug("trying to add up to "+available+ " push locs to pong"); long now = System.currentTimeMillis(); // Optimization: don't duplicate the written byte[] if not needed ByteArrayOutputStream baos = includeSize ? new ByteArrayOutputStream() : (ByteArrayOutputStream)out; while (pushlocs.hasNext() && available > 0) { PushAltLoc loc = pushlocs.next(); if (loc.getPushAddress().getProxies().isEmpty()) { pushlocs.remove(); continue; } if (loc.canBeSent(AlternateLocation.MESH_PING)) { baos.write(loc.getPushAddress().toBytes(includeTLS)); available--; loc.send(now,AlternateLocation.MESH_PING); } else if (!loc.canBeSentAny()) pushlocs.remove(); } if (baos.size() == 0) { //altlocs will not fit or none available - say we didn't send them LOG.debug("did not send any push locs"); return false; } else { LOG.debug("adding push altlocs"); if(includeSize) { ByteUtils.short2beb((short)baos.size(),out); baos.writeTo(out); } // else it's already written to out return true; } } /** * @param daos the output stream to write the ranges to * @return if they were written or not. */ private final boolean writeRanges(CountingOutputStream caos, FileDesc desc) throws IOException{ DataOutputStream daos = new DataOutputStream(caos); LOG.debug("adding ranges to pong"); IntervalSet.ByteIntervals ranges = deriveRanges(desc); // this is a non-ggep pong so we should not be serving long files. assert ranges.longs.length == 0 : "long ranges in legacy pong"; //write the ranges only if they will fit in the packet if (caos.getAmountWritten()+2 + ranges.ints.length <= PACKET_SIZE) { LOG.debug("added ranges"); daos.writeShort((short)ranges.ints.length); caos.write(ranges.ints); return true; } else { //the ranges will not fit - say we didn't send them. LOG.debug("ranges will not fit :("); return false; } } }