package com.limegroup.gnutella.messages; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import org.limewire.core.api.network.BandwidthCollector; import org.limewire.core.settings.SpeedConstants; import org.limewire.core.settings.UploadSettings; import org.limewire.io.IpPort; import org.limewire.io.NetworkUtils; import org.limewire.security.SecurityToken; import org.limewire.util.StringUtils; import com.google.inject.Inject; import com.google.inject.Singleton; import com.limegroup.gnutella.ApplicationServices; import com.limegroup.gnutella.ConnectionManager; import com.limegroup.gnutella.NetworkManager; import com.limegroup.gnutella.Response; import com.limegroup.gnutella.UploadManager; import com.limegroup.gnutella.util.DataUtils; import com.limegroup.gnutella.xml.LimeXMLDocumentHelper; import com.limegroup.gnutella.xml.LimeXMLUtils; @Singleton public class OutgoingQueryReplyFactoryImpl implements OutgoingQueryReplyFactory { private final QueryReplyFactory queryReplyFactory; private final UploadManager uploadManager; private final NetworkManager networkManager; private final ApplicationServices applicationServices; private final ConnectionManager connectionManager; private final BandwidthCollector bandwidthCollector; @Inject public OutgoingQueryReplyFactoryImpl(QueryReplyFactory queryReplyFactory, UploadManager uploadManager, NetworkManager networkManager, ApplicationServices applicationServices, ConnectionManager connectionManager, BandwidthCollector bandwidthCollector) { this.queryReplyFactory = queryReplyFactory; this.uploadManager = uploadManager; this.networkManager = networkManager; this.applicationServices = applicationServices; this.connectionManager = connectionManager; this.bandwidthCollector = bandwidthCollector; } public List<QueryReply> createReplies(Response[] responses, QueryRequest queryRequest, SecurityToken securityToken, int responsesPerReply) { // We only want to return a "reply to multicast query" QueryReply // if the request travelled a single hop. boolean isMulticast = queryRequest.isMulticast() && (queryRequest.getTTL() + queryRequest.getHops()) == 1; byte ttl = isMulticast ? 1 : (byte)(queryRequest.getHops() + 1); return createReplies(responses, responsesPerReply, securityToken, queryRequest.getGUID(), ttl, isMulticast, queryRequest.canDoFirewalledTransfer()); } public List<QueryReply> createReplies(Response[] responses, int responsesPerReply, SecurityToken securityToken, byte[] guid, byte ttl, boolean isMulticast, boolean requestorCanDoFWT) { List<Response[]> splitResponses = splitResponses(responses, responsesPerReply); List<QueryReply> replies = new ArrayList<QueryReply>(); for (Response[] bundle : splitResponses) { replies.addAll(createReplies(bundle, securityToken, guid, ttl, isMulticast, requestorCanDoFWT)); } return replies; } static List<Response[]> splitResponses(Response[] responses, int responsesPerReply) { if (responses.length <= responsesPerReply) { return Collections.singletonList(responses); } List<Response[]> results = new ArrayList<Response[]>(); for (int i = 0; i < responses.length; i += responsesPerReply) { Response[] copy = new Response[Math.min(responsesPerReply, responses.length - i)]; System.arraycopy(responses, i, copy, 0, copy.length); results.add(copy); } return results; } /** * @param securityToken might be null, otherwise must be sent in GGEP * of QHD with header "SO" */ public List<QueryReply> createReplies(Response[] responses, SecurityToken securityToken, byte[] guid, byte ttl, boolean isMulticast, boolean requestorCanDoFWT) { if (responses.length == 0) { return Collections.emptyList(); } // We should mark our hits if the remote end can do a firewalled // transfer AND so can we AND we don't accept tcp incoming AND our // external address is valid (needed for input into the reply) boolean isFWTransfer = requestorCanDoFWT && networkManager.canDoFWT() && !networkManager.acceptedIncomingConnection(); // see if there are any open slots // Note: if we are busy, non-metafile results would be filtered. // by this point. boolean isBusy = !uploadManager.mayBeServiceable(); boolean hasUploaded = uploadManager.hadSuccesfulUpload(); byte[] clientGUID = applicationServices.getMyGUID(); long speed = uploadManager.measuredUploadSpeed(); boolean measuredSpeed = true; if (speed == -1) { //measured speed in kilobits speed = bandwidthCollector.getMaxMeasuredTotalUploadBandwidth() * 8; if(speed <= 0) { //default to cable speed if no measurement have been done yet. //assume larger than modem to get better measurement stats. speed = SpeedConstants.CABLE_SPEED_INT; } measuredSpeed = false; } //max upload speed in kilobits int maxUploadSpeed = UploadSettings.MAX_UPLOAD_SPEED.getValue() / 1024 * 8; if(UploadSettings.LIMIT_MAX_UPLOAD_SPEED.getValue() && speed > maxUploadSpeed) { speed = maxUploadSpeed; } List<QueryReply> queryReplies = new ArrayList<QueryReply>(); // pick the right address & port depending on multicast & fwtrans // if we cannot find a valid address & port, exit early. int port = -1; byte[] ip = null; // first try using multicast addresses & ports, but if they're // invalid, fallback to non multicast. if(isMulticast) { ip = networkManager.getNonForcedAddress(); port = networkManager.getNonForcedPort(); if(!NetworkUtils.isValidPort(port) || !NetworkUtils.isValidAddress(ip)) isMulticast = false; } if(!isMulticast) { // see if we have a valid FWTrans address. if not, fall back. if(isFWTransfer) { port = networkManager.getStableUDPPort(); ip = networkManager.getExternalAddress(); if(!NetworkUtils.isValidAddress(ip) || !NetworkUtils.isValidPort(port)) isFWTransfer = false; } // if we still don't have a valid address here, exit early. if(!isFWTransfer) { ip = networkManager.getAddress(); port = networkManager.getPort(); if(!NetworkUtils.isValidAddress(ip) || !NetworkUtils.isValidPort(port)) return Collections.emptyList(); } } // get the *latest* push proxies if we have not accepted an incoming // connection in this session boolean notIncoming = !networkManager.acceptedIncomingConnection(); Set<? extends IpPort> proxies = notIncoming ? connectionManager.getPushProxies() : null; // if sending a single response, see if xml bytes have been constructed before already if (responses.length == 1) { byte[] compressedXmlBytes = responses[0].getCompressedXmlBytes(); if (compressedXmlBytes != null) { // create the new queryReply QueryReply queryReply = queryReplyFactory.createQueryReply(guid, ttl, port, ip, speed, responses, clientGUID, compressedXmlBytes, notIncoming, isBusy, hasUploaded, measuredSpeed, false /* chat */, isMulticast, isFWTransfer, proxies, securityToken); queryReplies.add(queryReply); return queryReplies; } } byte[] xmlBytes = createXmlBytes(responses); // it may be too big.... if (xmlBytes.length > QueryReply.XML_MAX_SIZE) { // ok, need to partition responses up once again and send out // multiple query replies..... List<Response[]> splitResps = new ArrayList<Response[]>(2); splitAndAddResponses(splitResps, responses); while (!splitResps.isEmpty()) { Response[] currResps = splitResps.remove(0); byte[] currXMLBytes = createXmlBytes(currResps); if ((currXMLBytes.length > QueryReply.XML_MAX_SIZE) && (currResps.length > 1)) splitAndAddResponses(splitResps, currResps); else { // create xml bytes if possible... byte[] xmlCompressed = LimeXMLUtils.compress(currXMLBytes); // create the new queryReply QueryReply queryReply = queryReplyFactory.createQueryReply(guid, ttl, port, ip, speed, currResps, clientGUID, xmlCompressed, notIncoming, isBusy, hasUploaded, measuredSpeed, false /* chat */, isMulticast, isFWTransfer, proxies, securityToken); queryReplies.add(queryReply); } } } else { // xml is small enough, no problem..... byte[] xmlCompressed = LimeXMLUtils.compress(xmlBytes); // create the new queryReply QueryReply queryReply = queryReplyFactory.createQueryReply(guid, ttl, port, ip, speed, responses, clientGUID, xmlCompressed, notIncoming, isBusy, hasUploaded, measuredSpeed, false /* chat */, isMulticast, isFWTransfer, proxies, securityToken); queryReplies.add(queryReply); } return queryReplies; } /** @return Simply splits the input array into two (almost) equally sized * arrays. */ private static Response[][] splitResponses(Response[] in) { int middle = in.length/2; Response[][] retResps = new Response[2][]; retResps[0] = new Response[middle]; retResps[1] = new Response[in.length-middle]; for (int i = 0; i < middle; i++) retResps[0][i] = in[i]; for (int i = 0; i < (in.length-middle); i++) retResps[1][i] = in[i+middle]; return retResps; } private static void splitAndAddResponses(List<Response[]> addTo, Response[] toSplit) { Response[][] splits = splitResponses(toSplit); addTo.add(splits[0]); addTo.add(splits[1]); } private byte[] createXmlBytes(Response...responses) { String xmlString = LimeXMLDocumentHelper.getAggregateString(responses); if (xmlString.isEmpty()) { return DataUtils.EMPTY_BYTE_ARRAY; } return StringUtils.toUTF8Bytes(xmlString); } @Override public byte[] getCompressedXmlBytes(Response response) { byte[] compressedXmlBytes = response.getCompressedXmlBytes(); if (compressedXmlBytes != null) { return compressedXmlBytes; } byte[] xmlBytes = createXmlBytes(response); if (xmlBytes.length > QueryReply.XML_MAX_SIZE) { response.setCompressedXmlBytes(DataUtils.EMPTY_BYTE_ARRAY); return DataUtils.EMPTY_BYTE_ARRAY; } else { xmlBytes = LimeXMLUtils.compress(xmlBytes); response.setCompressedXmlBytes(xmlBytes); return xmlBytes; } } }