package com.limegroup.gnutella.handshaking; import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.StringTokenizer; import org.limewire.core.settings.ApplicationSettings; import org.limewire.core.settings.ConnectionSettings; import org.limewire.io.IpPort; import org.limewire.util.Version; import org.limewire.util.VersionFormatException; import com.limegroup.gnutella.util.LimeWireUtils; /** * Contains the necessary information to form a response to a connection * handshake. It contains a status code, a status message, and the headers to * use in the response. * <p> * There are only two ways to create a HandshakeResponse. * <ol> * <li>Create an instance which defaults the status code and status message to * be "200 OK". Only the headers used in the response need to be passed in. * <li>Create an instance with a custom status code, status message, and the * headers used in the response. * </ol> */ public class HandshakeResponse { /** * Version of LimeWire we consider old. */ private static final Version OLD_LIME_VERSION; static { Version v = null; try { v = new Version("3.4.0"); } catch (VersionFormatException impossible) { } OLD_LIME_VERSION = v; } /** * The "default" status code in a connection handshake indicating that the * handshake was successful and the connection can be established. */ public static final int OK = 200; /** * The "default" status message in a connection handshake indicating that * the handshake was successful and the connection can be established. */ public static final String OK_MESSAGE = "OK"; /** * HTTP response code for the crawler. */ public static final int CRAWLER_CODE = 593; /** * HTTP response message for the crawler. */ public static final String CRAWLER_MESSAGE = "Hi"; /** * The error code that a shielded leaf node should give to incoming * connections. */ public static final int SHIELDED = 503; /** * The error code that a node with no slots should give to incoming * connections. */ public static final int SLOTS_FULL = 503; /** * Default bad status code to be used while rejecting connections. */ public static final int DEFAULT_BAD_STATUS_CODE = 503; /** * Default bad status message to be used while rejecting connections. */ public static final String DEFAULT_BAD_STATUS_MESSAGE = "Service Not Available"; /* * TODO: check about this error code... */ public static final int LOCALE_NO_MATCH = 577; public static final String LOCALE_NO_MATCH_MESSAGE = "Service Not Available"; /** * HTTP-like status code used when handshaking (e.g., 200, 401, 503). */ private final int STATUS_CODE; /** * Message used with status code when handshaking (e.g., "OK, "Service Not * Available"). The status message together with the status code make up the * status line (i.e., first line) of an HTTP-like response to a connection * handshake. */ private final String STATUS_MESSAGE; /** * Headers to use in the response to a connection handshake. */ private final Properties HEADERS; /** * Is the GGEP header set? */ private Boolean _supportsGGEP; /** * Cached boolean for whether or not this is considered a considered a * "good" leaf connection. */ private final boolean GOOD_LEAF; /** * Cached boolean for whether or not this is considered a considered a * "good" ultrapeer connection. */ private final boolean GOOD_ULTRAPEER; /** * Cached value for the number of Ultrapeers this Ultrapeer attempts to * connect to. */ private final int DEGREE; /** * Cached value for whether or not this is a high degree connection. */ private final boolean HIGH_DEGREE; /** * Cached value for whether or not this is an Ultrapeer connection that * supports Ultrapeer query routing. */ private final boolean ULTRAPEER_QRP; /** * Cached value for the maximum TTL to use along this connection. */ private final byte MAX_TTL; /** * Cached value for whether or not this connection supports dynamic * querying. */ private final boolean DYNAMIC_QUERY; /** * Cached value for whether or not this connection reported X-Ultrapeer: * true in it's handshake headers. */ private final boolean ULTRAPEER; /** * Cached value for whether or not this connection reported X-Ultrapeer: * false in it's handshake headers. */ private final boolean LEAF; /** * Cached value for whether or not the connection reported Content-Encoding: * deflate */ private final boolean DEFLATE_ENCODED; /** * Constant for whether or not this connection supports probe queries. */ private final boolean PROBE_QUERIES; /** * Constant for whether or not this node supports pong caching. */ private final boolean PONG_CACHING; /** * Constant for whether or not this node supports GUESS. */ private final boolean GUESS_CAPABLE; /** * Constant for whether or not this is a crawler. */ private final boolean IS_CRAWLER; /** * Constant for whether or not this node is a LimeWire (or derivative) */ private final boolean IS_LIMEWIRE; /** * Constant for whether or nor this node is an older limewire. */ private final boolean IS_OLD_LIMEWIRE; /** * Constant for whether or not the client claims to do no requerying. */ private final boolean NO_REQUERYING; /** * Locale */ private final String LOCALE_PREF; /** The port the response says it's listening on. */ private final int LISTEN_PORT; /** This connection's lime version */ private final Version limeVersion; /** * Constant for the number of hosts to return in X-Try-Ultrapeer headers. */ private static final int NUM_X_TRY_ULTRAPEER_HOSTS = 10; /** * Creates a <tt>HandshakeResponse</tt> which defaults the status code and * status message to be "200 Ok" and uses the desired headers in the * response. * * @param headers the headers to use in the response. */ // public for testing public HandshakeResponse(Properties headers) { this(OK, OK_MESSAGE, headers); } /** * Creates a new <tt>HandshakeResponse</tt> instance with the specified * response code and message and with no extra connection headers. * * @param code the status code for the response * @param message the status message */ protected HandshakeResponse(int code, String message) { this(code, message, new Properties()); } /** * Creates a HandshakeResponse with the desired status code, status message, * and headers to respond with. * * @param code the response code to use * @param message the response message to use * @param headers the headers to use in the response */ HandshakeResponse(int code, String message, Properties headers) { STATUS_CODE = code; STATUS_MESSAGE = message; HEADERS = headers; DEGREE = extractIntHeaderValue(HEADERS, HeaderNames.X_DEGREE, 6); HIGH_DEGREE = getNumIntraUltrapeerConnections() >= 15; ULTRAPEER_QRP = isVersionOrHigher(HEADERS, HeaderNames.X_ULTRAPEER_QUERY_ROUTING, 0.1F); MAX_TTL = extractByteHeaderValue(HEADERS, HeaderNames.X_MAX_TTL, (byte) 4); DYNAMIC_QUERY = isVersionOrHigher(HEADERS, HeaderNames.X_DYNAMIC_QUERY, 0.1F); PROBE_QUERIES = isVersionOrHigher(HEADERS, HeaderNames.X_PROBE_QUERIES, 0.1F); NO_REQUERYING = isFalseValue(HEADERS, HeaderNames.X_REQUERIES); IS_LIMEWIRE = extractStringHeaderValue(headers, HeaderNames.USER_AGENT).toLowerCase( Locale.US).startsWith("limewire"); GOOD_ULTRAPEER = isHighDegreeConnection() && isUltrapeerQueryRoutingConnection() && (getMaxTTL() < 5) && isDynamicQueryConnection(); GOOD_LEAF = GOOD_ULTRAPEER && (IS_LIMEWIRE || NO_REQUERYING); ULTRAPEER = isTrueValue(HEADERS, HeaderNames.X_ULTRAPEER); LEAF = isFalseValue(HEADERS, HeaderNames.X_ULTRAPEER); DEFLATE_ENCODED = isStringValue(HEADERS, HeaderNames.CONTENT_ENCODING, HeaderNames.DEFLATE_VALUE); PONG_CACHING = isVersionOrHigher(headers, HeaderNames.X_PONG_CACHING, 0.1F); GUESS_CAPABLE = isVersionOrHigher(headers, HeaderNames.X_GUESS, 0.1F); IS_CRAWLER = isVersionOrHigher(headers, HeaderNames.CRAWLER, 0.1F); Version version = null; if (IS_LIMEWIRE) { version = extractVersion(extractStringHeaderValue(headers, HeaderNames.USER_AGENT)); IS_OLD_LIMEWIRE = version != null && version.compareTo(OLD_LIME_VERSION) < 0; } else IS_OLD_LIMEWIRE = false; limeVersion = version; String loc = extractStringHeaderValue(headers, HeaderNames.X_LOCALE_PREF); LOCALE_PREF = (loc.equals("")) ? ApplicationSettings.DEFAULT_LOCALE.get() : loc; LISTEN_PORT = extractIntHeaderValueAfter(headers, HeaderNames.LISTEN_IP, ":", -1); } private Version extractVersion(String userAgent) { StringTokenizer tok = new StringTokenizer(userAgent, "/. "); if (tok.countTokens() < 3) return null; tok.nextToken(); // limewire String v = tok.nextToken() + "." + tok.nextToken() + "."; if (tok.hasMoreTokens()) v += tok.nextToken(); else v += "0"; try { return new Version(v); } catch (VersionFormatException vfe) { return null; } } private static final HandshakeResponse EMPTY_RESPONSE = new HandshakeResponse(new Properties()); /** * Creates an empty response with no headers. This is useful, for example, * during connection handshaking when we haven't yet read any headers. * * @return a new, empty <tt>HandshakeResponse</tt> instance */ public static HandshakeResponse createEmptyResponse() { return EMPTY_RESPONSE; } /** * Constructs the response from the other host during connection * handshaking. * * @return a new <tt>HandshakeResponse</tt> instance with the headers sent * by the other host */ public static HandshakeResponse createResponse(Properties headers) { return new HandshakeResponse(headers); } /** * Constructs the response from the other host during connection * handshaking. The returned response contains the connection headers sent * by the remote host. * * @param line the status line received from the connecting host * @param headers the headers received from the other host * @return a new <tt>HandshakeResponse</tt> instance with the headers sent * by the other host * @throws <tt>IOException</tt> if the status line could not be parsed */ public static HandshakeResponse createRemoteResponse(String line, Properties headers) throws IOException { int code = extractCode(line); if (code == -1) throw new IOException("could not parse status code: " + line); String message = extractMessage(line); if (message == null) throw new IOException("could not parse status message: " + line); return new HandshakeResponse(code, message, headers); } /** * Creates a new <tt>HandshakeResponse</tt> instance that accepts the * potential connection. * * @param headers the <tt>Properties</tt> instance containing the headers to * send to the node we're accepting */ static HandshakeResponse createAcceptIncomingResponse(HandshakeResponse response, Properties headers, HandshakeServices handshakeServices) { return new HandshakeResponse(addXTryHeader(response, headers, handshakeServices)); } /** * Creates a new <tt>HandshakeResponse</tt> instance that accepts the * outgoing connection -- the final third step in the handshake. This passes * no headers, as all necessary headers have already been exchanged. The * only possible exception is the potential inclusion of X-Ultrapeer: false. * * @param headers the <tt>Properties</tt> instance containing the headers to * send to the node we're accepting */ static HandshakeResponse createAcceptOutgoingResponse(Properties headers) { return new HandshakeResponse(headers); } /** * Creates a new <tt>HandshakeResponse</tt> instance that responds to a * special crawler connection with connected leaves and Ultrapeers. See the * Files>>Development section on the GDF. */ static HandshakeResponse createCrawlerResponse(HandshakeServices handshakeServices) { Properties headers = new Properties(); // add our user agent headers.put(HeaderNames.USER_AGENT, LimeWireUtils.getHttpServer()); headers.put(HeaderNames.X_ULTRAPEER, Boolean.toString(handshakeServices.isUltrapeer())); // add any leaves List<? extends IpPort> leaves = handshakeServices.getLeafNodes(); headers.put(HeaderNames.LEAVES, createEndpointString(leaves, leaves.size())); // add any Ultrapeers List<? extends IpPort> ultrapeers = handshakeServices.getUltrapeerNodes(); headers.put(HeaderNames.PEERS, createEndpointString(ultrapeers, ultrapeers.size())); return new HandshakeResponse(HandshakeResponse.CRAWLER_CODE, HandshakeResponse.CRAWLER_MESSAGE, headers); } /** * Creates a new <tt>HandshakeResponse</tt> instance that rejects the * potential connection. This includes the X-Try-Ultrapeers header to tell * the remote host about other nodes to connect to. We return the hosts we * most recently knew to have free leaf or ultrapeer connection slots. * * @param hr the <tt>HandshakeResponse</tt> containing the connection * headers of the connecting host * @return a <tt>HandshakeResponse</tt> with the appropriate response * headers */ static HandshakeResponse createUltrapeerRejectIncomingResponse(HandshakeResponse hr, HandshakeStatus status, HandshakeServices handshakeServices) { return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage(), addXTryHeader(hr, new Properties(), handshakeServices)); } /** * Creates a new <tt>HandshakeResponse</tt> instance that rejects the * potential connection. The returned <tt>HandshakeResponse</tt> DOES NOT * include the X-Try-Ultrapeers header because this is an outgoing * connection, and we should not send host data that the remote client does * not request. */ static HandshakeResponse createRejectOutgoingResponse(HandshakeStatus status) { return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage(), new Properties()); } /** * Creates a new <tt>HandshakeResponse</tt> instance that rejects the * potential connection to a leaf. We add hosts that we know about with free * connection slots to the X-Try-Ultrapeers header. * * @param hr the <tt>HandshakeResponse</tt> containing the headers of the * remote host * @return a new <tt>HandshakeResponse</tt> instance rejecting the * connection and with the specified connection headers */ static HandshakeResponse createLeafRejectIncomingResponse(HandshakeResponse hr, HandshakeStatus status, HandshakeServices handshakeServices) { return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage(), addXTryHeader(hr, new Properties(), handshakeServices)); } /** * Creates a new <tt>HandshakeResponse</tt> instance that rejects an * outgoing leaf connection. This occurs when we, as a leaf, reject a * connection on the third stage of the handshake. * * @return a new <tt>HandshakeResponse</tt> instance rejecting the * connection and with no extra headers */ static HandshakeResponse createLeafRejectOutgoingResponse(HandshakeStatus status) { return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage()); } static HandshakeResponse createLeafRejectLocaleOutgoingResponse() { return new HandshakeResponse(HandshakeResponse.LOCALE_NO_MATCH, HandshakeResponse.LOCALE_NO_MATCH_MESSAGE); } /** * Creates a new String of hosts, limiting the number of hosts to add to the * default value of 10. This is particularly used for the X-Try-Ultrapeers * header. * * @param hosts a <tt>Collection</tt> of <tt>IpPort</tt> instances * @return a string of the form IP:port,IP:port,... from the given list of * hosts * * Default access for testing. */ static String createEndpointString(Collection<? extends IpPort> hosts) { return createEndpointString(hosts, NUM_X_TRY_ULTRAPEER_HOSTS); } /** * Utility method that takes the specified list of hosts and returns a * string of the form: * <p> * * IP:port,IP:port,IP:port * * @param hosts a <tt>Collection</tt> of <tt>IpPort</tt> instances * @return a string of the form IP:port,IP:port,... from the given list of * hosts Default access for testing. */ static String createEndpointString(Collection<? extends IpPort> hosts, int limit) { StringBuilder sb = new StringBuilder(); int i = 0; Iterator<? extends IpPort> iter = hosts.iterator(); while (iter.hasNext() && i < limit) { IpPort host = iter.next(); sb.append(host.getAddress()); sb.append(":"); sb.append(host.getPort()); if (iter.hasNext()) { sb.append(","); } i++; } return sb.toString(); } /** * Utility method to extract the connection code from the connect string, * such as "200" in a "200 OK" message. * <p> * Default access for testing. * * @param line the full connection string, such as "200 OK." * @return the status code for the connection string, or -1 if the code * could not be parsed */ static int extractCode(String line) { // get the status code and message out of the status line int statusMessageIndex = line.indexOf(" "); if (statusMessageIndex == -1) return -1; try { return Integer.parseInt(line.substring(0, statusMessageIndex).trim()); } catch (NumberFormatException e) { return -1; } } /** * Utility method to extract the connection message from the connect string, * such as "OK" in a "200 OK" message. * <p> * Default access for testing. * * @param line the full connection string, such as "200 OK." * @return the status message for the connection string */ static String extractMessage(String line) { // get the status code and message out of the status line int statusMessageIndex = line.indexOf(" "); if (statusMessageIndex == -1) return null; return line.substring(statusMessageIndex).trim(); } /** * Utility method for creating a set of headers with the X-Try-Ultrapeers * header set according to the headers from the remote host. * <p> * Default access for testing. * @param hr the <tt>HandshakeResponse</tt> of the incoming request * @return a new <tt>Properties</tt> instance with the X-Try-Ultrapeers * header set according to the incoming headers from the remote host */ static Properties addXTryHeader(HandshakeResponse hr, Properties headers, HandshakeServices handshakeServices) { Collection<IpPort> hosts = handshakeServices.getAvailableHosts(hr.isUltrapeer(), hr .getLocalePref(), 10); headers.put(HeaderNames.X_TRY_ULTRAPEERS, createEndpointString(hosts)); return headers; } /** * Returns the response code. */ public int getStatusCode() { return STATUS_CODE; } /** * Returns the status message. * * @return the status message (e.g. "OK" , "Service Not Available" etc.) */ public String getStatusMessage() { return STATUS_MESSAGE; } /** * Tells if the status returned was OK or not. * * @return true, if the status returned was not the OK status, false * otherwise */ public boolean notOKStatusCode() { if (STATUS_CODE != OK) return true; else return false; } /** * Returns whether or not this connection was accepted -- whether or not the * connection returned Gnutella/0.6 200 OK. * * @return <tt>true</tt> if the server returned Gnutella/0.6 200 OK, * otherwise <tt>false</tt> */ public boolean isAccepted() { return STATUS_CODE == OK; } /** * Returns the status code and status message together used in a status * line. (e.g., "200 OK", "503 Service Not Available") */ public String getStatusLine() { return new String(STATUS_CODE + " " + STATUS_MESSAGE); } /** * Returns the headers as a <tt>Properties</tt> instance. */ public Properties props() { return HEADERS; } /** * Accessor for an individual property. */ public String getProperty(String prop) { return HEADERS.getProperty(prop); } /** * Returns the vendor string reported by this connection, i.e., the * USER_AGENT property, or null if it wasn't set. * * @return the vendor string, or null if unknown */ public String getUserAgent() { return HEADERS.getProperty(HeaderNames.USER_AGENT); } /** * Returns the maximum TTL that queries originating from us and sent from * this connection should have. If the max TTL header is not present, the * default TTL is assumed. * * @return the maximum TTL that queries sent to this connection should have * -- this will always be 5 or less */ public byte getMaxTTL() { return MAX_TTL; } /** * Accessor for the X-Try-Ultrapeers header. If the header does not exist or * is empty, this returns the empty string. * * @return the string of X-Try-Ultrapeer hosts, or the empty string if they * do not exist */ public String getXTryUltrapeers() { return extractStringHeaderValue(HEADERS, HeaderNames.X_TRY_ULTRAPEERS); } /** * This is a convenience method to see if the connection passed the * X-Try-Ultrapeer header. This simply checks the existence of the header -- * if the header was sent but is empty, this still returns <tt>true</tt>. * * @return <tt>true</tt> if this connection sent the X-Try-Ultrapeer header, * otherwise <tt>false</tt> */ public boolean hasXTryUltrapeers() { return headerExists(HEADERS, HeaderNames.X_TRY_ULTRAPEERS); } /** * Returns whether or not this host included leaf guidance, i.e., whether or * not the host wrote: * <p> * X-Ultrapeer-Needed: false * * @return <tt>true</tt> if the other host returned X-Ultrapeer-Needed: * false, otherwise <tt>false</tt> */ public boolean hasLeafGuidance() { return isFalseValue(HEADERS, HeaderNames.X_ULTRAPEER_NEEDED); } /** * Returns the number of intra-Ultrapeer connections this node maintains. * * @return the number of intra-Ultrapeer connections this node maintains */ public int getNumIntraUltrapeerConnections() { return DEGREE; } // implements ReplyHandler interface -- inherit doc comment public boolean isHighDegreeConnection() { return HIGH_DEGREE; } /** * Returns whether or not we think this connection is from a LimeWire or a * derivative of LimeWire. */ public boolean isLimeWire() { return IS_LIMEWIRE; } /** * @return true if we consider this an older version of LimeWire, false * otherwise */ public boolean isOldLimeWire() { return IS_OLD_LIMEWIRE; } /** * @return the version of this connection if its LimeWire. null if not * LimeWire or could not be parsed. */ public Version getLimeVersion() { return limeVersion; } /** * Returns whether or not this is connection passed the headers to be * considered a "good" leaf. * * @return <tt>true</tt> if this is considered a "good" leaf, otherwise * <tt>false</tt> */ public boolean isGoodLeaf() { return GOOD_LEAF; } /** * Returns whether or not this connection is encoded in deflate. */ public boolean isDeflateEnabled() { // this does NOT check the setting because we have already told the // outgoing side we support encoding, and they're expecting us to use it return DEFLATE_ENCODED; } /** * Returns whether or not this connection accepts deflate as an encoding. */ public boolean isDeflateAccepted() { // Note that we check the ENCODE_DEFLATE setting, and NOT the // ACCEPT_DEFLATE setting. This is a trick to prevent the // HandshakeResponders from thinking they can encode // the via deflate if we do not want to encode in deflate. return ConnectionSettings.ENCODE_DEFLATE.getValue() && containsStringValue(HEADERS, // the // headers // to // look // through HeaderNames.ACCEPT_ENCODING,// the header to look for HeaderNames.DEFLATE_VALUE); // the value to look for } /** * Returns whether or not this is connection passed the headers to be * considered a "good" ultrapeer. * * @return <tt>true</tt> if this is considered a "good" ultrapeer, otherwise * <tt>false</tt> */ public boolean isGoodUltrapeer() { return GOOD_ULTRAPEER; } /** * Returns whether or not this connection supports query routing between * Ultrapeers at 1 hop. * * @return <tt>true</tt> if this is an Ultrapeer connection that exchanges * query routing tables with other Ultrapeers at 1 hop, otherwise * <tt>false</tt> */ public boolean isUltrapeerQueryRoutingConnection() { return ULTRAPEER_QRP; } /** * Returns true iff this connection wrote "X-Ultrapeer: false". This does * NOT necessarily mean the connection is shielded. */ public boolean isLeaf() { return LEAF; } /** Returns true iff this connection wrote "X-Ultrapeer: true". */ public boolean isUltrapeer() { return ULTRAPEER; } /** * Returns whether or not this connection is to a client supporting GUESS. * * @return <tt>true</tt> if the node on the other end of this connection * supports GUESS, <tt>false</tt> otherwise */ public boolean isGUESSCapable() { return GUESS_CAPABLE; } /** * Returns whether or not this connection is to a ultrapeer supporting * GUESS. * * @return <tt>true</tt> if the node on the other end of this Ultrapeer * connection supports GUESS, <tt>false</tt> otherwise */ public boolean isGUESSUltrapeer() { return isGUESSCapable() && isUltrapeer(); } /** * Returns true iff this connection is a temporary connection as per the * headers. */ public boolean isTempConnection() { // get the X-Temp-Connection from either the headers received String value = HEADERS.getProperty(HeaderNames.X_TEMP_CONNECTION); // if X-Temp-Connection header is not received, return false, else // return the value received if (value == null) return false; else return Boolean.valueOf(value).booleanValue(); } /** * Returns true if this supports GGEP'ed messages. GGEP'ed messages (e.g., * big pongs) should only be sent along connections for which * supportsGGEP()==true. */ public boolean supportsGGEP() { if (_supportsGGEP == null) { String value = HEADERS.getProperty(HeaderNames.GGEP); // Currently we don't care about the version number. _supportsGGEP = new Boolean(value != null); } return _supportsGGEP.booleanValue(); } /** * Determines whether or not this node supports vendor messages. * * @return <tt>true</tt> if this node supports vendor messages, otherwise * <tt>false</tt> */ public float supportsVendorMessages() { String value = HEADERS.getProperty(HeaderNames.X_VENDOR_MESSAGE); if ((value != null) && !value.equals("")) { try { return Float.parseFloat(value); } catch (NumberFormatException nfe) { return 0; } } return 0; } /** * Returns whether or not this node supports pong caching. * * @return <tt>true</tt> if this node supports pong caching, otherwise * <tt>false</tt> */ public boolean supportsPongCaching() { return PONG_CACHING; } public String getVersion() { return HEADERS.getProperty(HeaderNames.X_VERSION); } /** * True if the remote host supports query routing (QRP). This is only * meaningful in the context of leaf-supernode relationships. */ public boolean isQueryRoutingEnabled() { return isVersionOrHigher(HEADERS, HeaderNames.X_QUERY_ROUTING, 0.1F); } /** * Returns whether or not the node on the other end of this connection uses * dynamic querying. * * @return <tt>true</tt> if this node uses dynamic querying, otherwise * <tt>false</tt> */ public boolean isDynamicQueryConnection() { return DYNAMIC_QUERY; } /** * Accessor for whether or not this connection supports TTL=1 probe queries. * These queries are treated separately from other queries. In particular, * if a second query with the same GUID is received, it is not considered a * duplicate. * * @return <tt>true</tt> if this connection supports probe queries, * otherwise <tt>false</tt> */ public boolean supportsProbeQueries() { return PROBE_QUERIES; } /** * Determines whether or not this handshake is from the crawler. * * @return <tt>true</tt> if this handshake is from the crawler, otherwise * <tt>false</tt> */ public boolean isCrawler() { return IS_CRAWLER; } /** * Access the locale preferences advertised by the client. */ public String getLocalePref() { return LOCALE_PREF; } /** Accessor for the listening port. */ public int getListeningPort() { return LISTEN_PORT; } /** * Convenience method that returns whether or not the given header exists. * * @return <tt>true</tt> if the header exists, otherwise <tt>false</tt> */ private static boolean headerExists(Properties headers, String headerName) { String value = headers.getProperty(headerName); return value != null; } /** * Utility method for checking whether or not a given header value is true. * * @param headers the headers to check * @param headerName the header name to look for */ private static boolean isTrueValue(Properties headers, String headerName) { String value = headers.getProperty(headerName); if (value == null) return false; return Boolean.valueOf(value).booleanValue(); } /** * Utility method for checking whether or not a given header value is false. * * @param headers the headers to check * @param headerName the header name to look for */ private static boolean isFalseValue(Properties headers, String headerName) { String value = headers.getProperty(headerName); if (value == null) return false; return value.equalsIgnoreCase("false"); } /** * Utility method for determining whether or not a given header is a given * string value. Case-insensitive. * * @param headers the headers to check * @param headerName the headerName to look for * @param headerValue the headerValue to check against */ private static boolean isStringValue(Properties headers, String headerName, String headerValue) { String value = headers.getProperty(headerName); if (value == null) return false; return value.equalsIgnoreCase(headerValue); } /** * Utility method for determining whether or not a given header contains a * given string value within a comma-delimited list. Case-insensitive. * * @param headers the headers to check * @param headerName the headerName to look for * @param headerValue the headerValue to check against */ private static boolean containsStringValue(Properties headers, String headerName, String headerValue) { String value = headers.getProperty(headerName); if (value == null) return false; // As a small optimization, we first check to see if the value // by itself is what we want, so we don't have to create the // StringTokenizer. if (value.equalsIgnoreCase(headerValue)) return true; StringTokenizer st = new StringTokenizer(value, ","); while (st.hasMoreTokens()) { if (st.nextToken().equalsIgnoreCase(headerValue)) return true; } return false; } /** * Utility method that checks the headers to see if the advertised version * for a specified feature is greater than or equal to the version we * require (<tt>minVersion</tt>. * * @param headers the connection headers to evaluate * @param headerName the header name for the feature to check * @param minVersion the minimum version that we require for this feature * * @return <tt>true</tt> if the version number for the specified feature is * greater than or equal to <tt>minVersion</tt>, otherwise * <tt>false</tt>. */ private static boolean isVersionOrHigher(Properties headers, String headerName, float minVersion) { String value = headers.getProperty(headerName); if (value == null) return false; try { Float f = new Float(value); return f.floatValue() >= minVersion; } catch (NumberFormatException e) { return false; } } /** * Helper method for returning an int header value. If the header name is * not found, or if the header value cannot be parsed, the default value is * returned. * * @param headers the connection headers to search through * @param headerName the header name to look for * @param defaultValue the default value to return if the header value could * not be properly parsed * @return the int value for the header */ private static int extractIntHeaderValue(Properties headers, String headerName, int defaultValue) { String value = headers.getProperty(headerName); if (value == null) return defaultValue; try { return Integer.valueOf(value).intValue(); } catch (NumberFormatException e) { return defaultValue; } } /** * Helper method for returning an int header value in a header after a * certain character. If the header name is not found, or if the header * value cannot be parsed, the default value is returned. * * @param headers the connection headers to search through * @param headerName the header name to look for * @param token the token after which the int is looked for * @param defaultValue the default value to return if the header value could * not be properly parsed * @return the int value for the header */ private static int extractIntHeaderValueAfter(Properties headers, String headerName, String token, int defaultValue) { String value = headers.getProperty(headerName); if (value == null) return defaultValue; int idx = value.indexOf(token) + 1; if (idx == 0 || idx == value.length()) return defaultValue; try { return Integer.valueOf(value.substring(idx)).intValue(); } catch (NumberFormatException e) { return defaultValue; } } /** * Helper method for returning a byte header value. If the header name is * not found, or if the header value cannot be parsed, the default value is * returned. * * @param headers the connection headers to search through * @param headerName the header name to look for * @param defaultValue the default value to return if the header value could * not be properly parsed * @return the byte value for the header */ private static byte extractByteHeaderValue(Properties headers, String headerName, byte defaultValue) { String value = headers.getProperty(headerName); if (value == null) return defaultValue; try { return Byte.valueOf(value).byteValue(); } catch (NumberFormatException e) { return defaultValue; } } /** * Helper method for returning a string header value. If the header name is * not found, or if the header value cannot be parsed, the default value is * returned. * * @param headers the connection headers to search through * @param headerName the header name to look for * @return the string value for the header, or the empty string if the * header could not be found */ private static String extractStringHeaderValue(Properties headers, String headerName) { String value = headers.getProperty(headerName); if (value == null) return ""; return value; } @Override public String toString() { return "<" + STATUS_CODE + ", " + STATUS_MESSAGE + ">" + HEADERS; } }