package com.limegroup.gnutella; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.http.Header; import org.apache.http.HttpException; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.SingleClientConnManager; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.limewire.core.api.browse.BrowseListener; import org.limewire.friend.api.FriendPresence; import org.limewire.friend.api.feature.AddressFeature; import org.limewire.friend.api.feature.AuthTokenFeature; import org.limewire.friend.api.feature.Feature; import org.limewire.http.httpclient.SocketWrapperProtocolSocketFactory; import org.limewire.io.Address; import org.limewire.io.GUID; import org.limewire.io.IOUtils; import org.limewire.io.NetworkUtils; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.net.BlockingConnectObserver; import org.limewire.net.SocketsManager; import org.limewire.util.StringUtils; import com.google.inject.Provider; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.Message.Network; import com.limegroup.gnutella.messages.MessageFactory; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.util.LimeWireUtils; /** * 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 CONNECTING = 5; static final int DIRECT_CONNECT_TIME = 10000; // 10 seconds. private static final long EXPIRE_TIME = 15000; // 15 seconds /** The GUID to be used for incoming QRs from the Browse Request. */ private GUID _guid = 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; private final SocketsManager socketsManager; private final Provider<ForMeReplyHandler> forMeReplyHandler; private final MessageFactory messageFactory; private final Provider<HttpParams> httpParams; private final NetworkManager networkManager; private final PushEndpointFactory pushEndpointFactory; /** * @param sessionGuid The GUID you have associated on the front end with the * results of this Browse Host request. * @param clientProvider used to make an HTTP client request over an *incoming* Socket */ BrowseHostHandler(GUID sessionGuid, SocketsManager socketsManager, Provider<ForMeReplyHandler> forMeReplyHandler, MessageFactory messageFactory, Provider<HttpParams> httpParams, NetworkManager networkManager, PushEndpointFactory pushEndpointFactory) { _guid = sessionGuid; this.socketsManager = socketsManager; this.forMeReplyHandler = forMeReplyHandler; this.messageFactory = messageFactory; this.httpParams = httpParams; this.networkManager = networkManager; this.pushEndpointFactory = pushEndpointFactory; } public void browseHost(FriendPresence friendPresence, BrowseListener browseListener) { setState(STARTED); setState(CONNECTING); try { AddressFeature addressFeature = (AddressFeature)friendPresence.getFeature(AddressFeature.ID); if(addressFeature != null) { if (LOG.isDebugEnabled()) { LOG.debug("browsing address: " + addressFeature.getFeature()); } Socket socket = socketsManager.connect(addressFeature.getFeature(), new BlockingConnectObserver()).getSocket(EXPIRE_TIME, TimeUnit.MILLISECONDS); browseHost(socket, friendPresence); browseListener.browseFinished(true); return; } } catch (IOException ie) { if (LOG.isInfoEnabled()) LOG.info("Error during browse host: " + friendPresence, ie); } catch (URISyntaxException e) { LOG.info("Error during browse host", e); } catch (HttpException e) { LOG.info("Error during browse host", e); } catch (InterruptedException e) { LOG.info("Error during browse host", e); } catch (TimeoutException e) { LOG.info("Error during browse host", e); } browseListener.browseFinished(false); failed(); } /** * 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: case CONNECTING: // 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. */ void failed() { setState(FINISHED); } void browseHost(Socket socket, FriendPresence friendPresence) throws IOException, URISyntaxException, HttpException, InterruptedException { try { setState(EXCHANGING); HttpResponse response = makeHTTPRequest(socket, friendPresence); validateResponse(response); readQueryRepliesFromStream(response, friendPresence); } finally { IOUtils.close(socket); setState(FINISHED); } } private HttpResponse makeHTTPRequest(Socket socket, FriendPresence friendPresence) throws IOException, URISyntaxException, HttpException, InterruptedException { // SocketWrappingHttpClient client = clientProvider.get(); // client.setSocket(socket); SocketWrappingHttpClient client = new SocketWrappingHttpClient(socket); if(!friendPresence.getFriend().isAnonymous()) { String username = friendPresence.getFriend().getNetwork().getCanonicalizedLocalID(); Feature feature = friendPresence.getFeature(AuthTokenFeature.ID); if(feature != null) { AuthTokenFeature authTokenFeature = (AuthTokenFeature)feature; String password = StringUtils.toUTF8String(authTokenFeature.getFeature().getToken()); client.setCredentials(new UsernamePasswordCredentials(username, password)); } else { LOG.infof("no auth token for: {0}", friendPresence); } } // hardcoding to "http" should work; // socket has already been established HttpGet get = new HttpGet("http://" + NetworkUtils.ip2string(socket.getInetAddress().getAddress()) + ":" + socket.getPort() + getPath(friendPresence)); HttpProtocolParams.setVersion(client.getParams(), HttpVersion.HTTP_1_1); get.addHeader(HTTPHeaderName.HOST.create(NetworkUtils.ip2string(socket.getInetAddress().getAddress()) + ":" + socket.getPort())); get.addHeader(HTTPHeaderName.USER_AGENT.create(LimeWireUtils.getVendor())); get.addHeader(HTTPHeaderName.ACCEPT.create(Constants.QUERYREPLY_MIME_TYPE)); get.addHeader(HTTPHeaderName.CONNECTION.create(HTTP.CONN_KEEP_ALIVE)); if (!networkManager.acceptedIncomingConnection() && networkManager.canDoFWT()) { get.addHeader(HTTPHeaderName.FW_NODE_INFO.create(pushEndpointFactory.createForSelf())); } return client.execute(get); } String getPath(FriendPresence friendPresence) { if(friendPresence.getFriend().isAnonymous()) { return "/"; } else { try { return "/friend/browse/" + URLEncoder.encode(friendPresence.getFriend().getNetwork().getCanonicalizedLocalID(), "UTF-8") + "/"; } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } private void validateResponse(HttpResponse response) throws IOException { if(response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) { throw new IOException("HTTP status code = " + response.getStatusLine().getStatusCode()); // TODO create Exception class containing http status code } Header contentType = response.getFirstHeader("Content-Type"); if(contentType != null && StringUtils.indexOfIgnoreCase(contentType.getValue(), Constants.QUERYREPLY_MIME_TYPE, Locale.ENGLISH) < 0) { // TODO concat all values throw new IOException("Unsupported Content-Type: " + contentType.getValue()); } Header contentEncoding = response.getFirstHeader("Content-Encoding"); if(contentEncoding != null) { // TODO - define acceptable encoding? throw new IOException("Unsupported Content-Encoding: " + contentEncoding.getValue()); } Header contentLength = response.getFirstHeader("Content-Length"); if(contentLength != null) { try { _replyLength = Long.parseLong(contentLength.getValue()); } catch (NumberFormatException nfe) { } } } private void readQueryRepliesFromStream(HttpResponse response, FriendPresence friendPresence) { AddressFeature addressFeature = (AddressFeature)friendPresence.getFeature(AddressFeature.ID); if(response.getEntity() != null && addressFeature != null) { // address can be null if either party is concurrently logging out Address address = addressFeature.getFeature(); InputStream in; try { in = response.getEntity().getContent(); } catch (IOException e) { LOG.info("Unable to read a single message", e); return; } Message m = null; while(true) { try { m = null; LOG.debug("reading message"); m = messageFactory.read(in, Network.TCP); } catch (BadPacketException bpe) { LOG.debug("BPE while reading", bpe); } catch (IOException expected){ LOG.debug("IOE while reading", expected); } // either timeout, or the remote closed. if(m == null) { LOG.debug("Unable to read create message"); return; } else { if(m instanceof QueryReply) { _currentLength += m.getTotalLength(); if(LOG.isTraceEnabled()) LOG.trace("BHH.browseExchange(): from " + friendPresence + " read QR:" + m); QueryReply reply = (QueryReply)m; reply.setGUID(_guid); reply.setBrowseHostReply(true); forMeReplyHandler.get().handleQueryReply(reply, null, address); } } } } } public static class PushRequestDetails { private FriendPresence friendPresence; private BrowseHostHandler bhh; private long timeStamp; public PushRequestDetails(BrowseHostHandler bhh, FriendPresence friendPresence) { this.friendPresence = friendPresence; timeStamp = System.currentTimeMillis(); this.bhh = bhh; } public boolean isExpired() { return ((System.currentTimeMillis() - timeStamp) > EXPIRE_TIME); } public BrowseHostHandler getBrowseHostHandler() { return bhh; } public FriendPresence getFriendPresence() { return friendPresence; } } class SocketWrappingHttpClient extends DefaultHttpClient { private Credentials credentials; SocketWrappingHttpClient(Socket socket) { super(new SingleClientConnManager(httpParams.get(), getSchemeRegistry(socket)), httpParams.get()); } void setCredentials(Credentials credentials) { this.credentials = credentials; } @Override protected CredentialsProvider createCredentialsProvider() { return new CredentialsProvider() { public void setCredentials(AuthScope authscope, Credentials credentials) { throw new UnsupportedOperationException(); } public Credentials getCredentials(AuthScope authscope) { return credentials; } public void clear() { credentials = null; } }; } /** * @return an <code>HttpRequestRetryHandler</code> that always returns * <code>false</code> */ @Override protected HttpRequestRetryHandler createHttpRequestRetryHandler() { return new HttpRequestRetryHandler() { public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { // when requests fail for unexpected reasons (eg., IOException), we don't // want to blindly re-attempt return false; } }; } } private static SchemeRegistry getSchemeRegistry(Socket socket) { SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", new SocketWrapperProtocolSocketFactory(socket), 80)); schemeRegistry.register(new Scheme("tls", new SocketWrapperProtocolSocketFactory(socket),80)); schemeRegistry.register(new Scheme("https", new SocketWrapperProtocolSocketFactory(socket),80)); return schemeRegistry; } }