/* * Created on Nov 12, 2003 * Created by Alon Rohter * Copyright (C) 2003-2004 Alon Rohter, All Rights Reserved. * Copyright (C) 2003, 2004, 2005, 2006 Aelitis, All Rights Reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * AELITIS, SAS au capital de 46,603.30 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. * */ package com.aelitis.azureus.core.peermanager.utils; //import org.gudy.azureus2.core3.internat.MessageText; //import org.gudy.azureus2.core3.util.AEDiagnostics; //import org.gudy.azureus2.core3.util.AEDiagnosticsLogger; import org.gudy.azureus2.core3.util.ByteFormatter; import org.gudy.azureus2.core3.util.Constants; //import org.gudy.azureus2.core3.util.Debug; import java.io.*; import java.util.HashSet; /** * Used for identifying clients by their peerID. */ public class BTPeerIDByteDecoder { final static boolean LOG_UNKNOWN; static { String prop = System.getProperty("log.unknown.peerids"); LOG_UNKNOWN = prop == null || prop.equals("1"); } private static String logUnknownClient0(byte[] peer_id_bytes) throws IOException { String text = new String(peer_id_bytes, 0, 20, Constants.BYTE_ENCODING); text = text.replace((char)12, (char)32); text = text.replace((char)10, (char)32); return "[" + text + "] " + ByteFormatter.encodeString(peer_id_bytes) + " "; } private static String asUTF8ByteString(String text) { try { byte[] utf_bytes = text.getBytes(Constants.DEFAULT_ENCODING); return ByteFormatter.encodeString(utf_bytes); } catch (UnsupportedEncodingException uee) {return "";} } private static HashSet logged_discrepancies = new HashSet(); public static void logClientDiscrepancy(String peer_id_name, String handshake_name, String discrepancy, String protocol, byte[] peer_id) { if (!client_logging_allowed) {return;} // Generate the string used that we will log. String line_to_log = discrepancy + " [" + protocol + "]: "; line_to_log += "\"" + peer_id_name + "\" / \"" + handshake_name + "\" "; // We'll encode the name in byte form to help us decode it. line_to_log += "[" + asUTF8ByteString(handshake_name) + "]"; // Avoid logging the same combination of things again. boolean log_to_debug_out = Constants.isCVSVersion(); if (log_to_debug_out || LOG_UNKNOWN) { // If this text has been recorded before, then avoid doing it again. if (!logged_discrepancies.add(line_to_log)) {return;} } // Add peer ID bytes. if (peer_id != null) { line_to_log += ", Peer ID: " + ByteFormatter.encodeString(peer_id); } // Enable this block for now - just until we get more feedback about // problematic clients. if (log_to_debug_out) { // Debug.outNoStack("Conflicting peer identification: " + line_to_log); } if (!LOG_UNKNOWN) {return;} // logClientDiscrepancyToFile(line_to_log); } // private static AEDiagnosticsLogger logger = null; // private synchronized static void logClientDiscrepancyToFile(String line_to_log) { // if (logger == null) {logger = AEDiagnostics.getLogger("clientid");} // try {logger.log(line_to_log);} // catch (Throwable e) {Debug.printStackTrace(e);} // } static boolean client_logging_allowed = true; // I don't expect this to grow too big, and it won't grow if there's no logging going on. private static HashSet logged_ids = new HashSet(); static void logUnknownClient(byte[] peer_id_bytes) {logUnknownClient(peer_id_bytes, true);} static void logUnknownClient(byte[] peer_id_bytes, boolean to_debug_out) { if (!client_logging_allowed) {return;} // Avoid logging the same client ID multiple times. boolean log_to_debug_out = to_debug_out && Constants.isCVSVersion(); if (log_to_debug_out || LOG_UNKNOWN) { // If the ID has been recorded before, then avoid doing it again. if (!logged_ids.add(makePeerIDReadableAndUsable(peer_id_bytes))) {return;} } // Enable this block for now - just until we get more feedback about // unknown clients. if (log_to_debug_out) { // Debug.outNoStack("Unable to decode peer correctly - peer ID bytes: " + makePeerIDReadableAndUsable(peer_id_bytes)); } if (!LOG_UNKNOWN) {return;} // try {logClientDiscrepancyToFile(logUnknownClient0(peer_id_bytes));} // catch (Throwable t) {Debug.printStackTrace(t);} } static void logUnknownClient(String peer_id) { try {logUnknownClient(peer_id.getBytes(Constants.BYTE_ENCODING));} catch (UnsupportedEncodingException uee) {} } public static String decode0(byte[] peer_id_bytes) { // final String UNKNOWN = MessageText.getString("PeerSocket.unknown"); // final String FAKE = MessageText.getString("PeerSocket.fake_client"); // final String BAD_PEER_ID = MessageText.getString("PeerSocket.bad_peer_id"); String peer_id = null; try {peer_id = new String(peer_id_bytes, Constants.BYTE_ENCODING);} catch (UnsupportedEncodingException uee) {return "";} // We store the result here. String client = null; /** * If the client reuses parts of the peer ID of other peers, then try to determine this * first (before we misidentify the client). */ if (BTPeerIDByteDecoderUtils.isPossibleSpoofClient(peer_id)) { client = decodeBitSpiritClient(peer_id, peer_id_bytes); if (client != null) {return client;} client = decodeBitCometClient(peer_id, peer_id_bytes); if (client != null) {return client;} // return "BitSpirit? (" + BAD_PEER_ID + ")"; } /** * See if the client uses Az style identification. */ if (BTPeerIDByteDecoderUtils.isAzStyle(peer_id)) { client = BTPeerIDByteDecoderDefinitions.getAzStyleClientName(peer_id); if (client != null) { String client_with_version = BTPeerIDByteDecoderDefinitions.getAzStyleClientVersion(client, peer_id); /** * Hack for fake ZipTorrent clients - there seems to be some clients * which use the same identifier, but they aren't valid ZipTorrent clients. */ if (client.startsWith("ZipTorrent") && peer_id.startsWith("bLAde", 8)) { String client_name = (client_with_version == null) ? client : client_with_version; // return UNKNOWN + " [" + FAKE + ": " + client_name + "]"; } /** * BitTorrent 6.0 Beta currently misidentifies itself. */ if ("\u00B5Torrent 6.0.0 Beta".equals(client_with_version)) { return "Mainline 6.0 Beta"; } /** * If it's the rakshasa libtorrent, then it's probably rTorrent. */ if (client.startsWith("libTorrent (Rakshasa)")) { String client_name = (client_with_version == null) ? client : client_with_version; return client_name + " / rTorrent*"; } if (client_with_version != null) {return client_with_version;} return client; } } /** * See if the client uses Shadow style identification. */ if (BTPeerIDByteDecoderUtils.isShadowStyle(peer_id)) { client = BTPeerIDByteDecoderDefinitions.getShadowStyleClientName(peer_id); if (client != null) { String client_ver = BTPeerIDByteDecoderUtils.getShadowStyleVersionNumber(peer_id); if (client_ver != null) {return client + " " + client_ver;} return client; } } /** * See if the client uses Mainline style identification. */ client = BTPeerIDByteDecoderDefinitions.getMainlineStyleClientName(peer_id); if (client != null) { /** * We haven't got a good way of detecting whether this is a Mainline style * version of peer ID until we start decoding peer ID information. So for * that reason, we wait until we get client version information here - if * we don't manage to determine a version number, then we assume that it * has been misidentified and carry on with it. */ String client_ver = BTPeerIDByteDecoderUtils.getMainlineStyleVersionNumber(peer_id); if (client_ver != null) { String result = client + " " + client_ver; return result; } } /** * Check for BitSpirit / BitComet (non possible spoof client mode). */ client = decodeBitSpiritClient(peer_id, peer_id_bytes); if (client != null) {return client;} client = decodeBitCometClient(peer_id, peer_id_bytes); if (client != null) {return client;} /** * See if the client identifies itself using a particular substring. */ BTPeerIDByteDecoderDefinitions.ClientData client_data = BTPeerIDByteDecoderDefinitions.getSubstringStyleClient(peer_id); if (client_data != null) { client = client_data.client_name; String client_with_version = BTPeerIDByteDecoderDefinitions.getSubstringStyleClientVersion(client_data, peer_id, peer_id_bytes); if (client_with_version != null) {return client_with_version;} return client; } client = identifyAwkwardClient(peer_id_bytes); if (client != null) {return client;} return null; } /** * Decodes the given peerID, returning an identification string. */ public static String decode(byte[] peer_id) { if ( peer_id.length > 0 ){ try { String client = decode0(peer_id); if (client != null ){ return client; } }catch (Throwable e) { // Debug.out( "Failed to decode peer id " + ByteFormatter.encodeString(peer_id) + ": " + Debug.getNestedExceptionMessageAndStack( e )); } try { String peer_id_as_string = new String(peer_id, Constants.BYTE_ENCODING); boolean is_az_style = BTPeerIDByteDecoderUtils.isAzStyle(peer_id_as_string); boolean is_shadow_style = BTPeerIDByteDecoderUtils.isShadowStyle(peer_id_as_string); logUnknownClient(peer_id, !(is_az_style || is_shadow_style)); if (is_az_style) { return BTPeerIDByteDecoderDefinitions.formatUnknownAzStyleClient(peer_id_as_string); } else if (is_shadow_style) { return BTPeerIDByteDecoderDefinitions.formatUnknownShadowStyleClient(peer_id_as_string); } }catch( Throwable e ){ // Debug.out( "Failed to decode peer id " + ByteFormatter.encodeString(peer_id) + ": " + Debug.getNestedExceptionMessageAndStack( e )); } } String sPeerID = getPrintablePeerID(peer_id); // return MessageText.getString("PeerSocket.unknown") + " [" + sPeerID + "]"; return "PeerSocket unknown"; } public static String identifyAwkwardClient(byte[] peer_id) { int iFirstNonZeroPos = 0; iFirstNonZeroPos = 20; for( int i=0; i < 20; i++ ) { if( peer_id[i] != (byte)0 ) { iFirstNonZeroPos = i; break; } } //Shareaza check if( iFirstNonZeroPos == 0 ) { boolean bShareaza = true; for( int i=0; i < 16; i++ ) { if( peer_id[i] == (byte)0 ) { bShareaza = false; break; } } if( bShareaza ) { for( int i=16; i < 20; i++ ) { if( peer_id[i] != ( peer_id[i % 16] ^ peer_id[15 - (i % 16)] ) ) { bShareaza = false; break; } } if( bShareaza ) return "Shareaza"; } } byte three = (byte)3; if ((iFirstNonZeroPos == 9) && (peer_id[9] == three) && (peer_id[10] == three) && (peer_id[11] == three)) { return "Snark"; } if ((iFirstNonZeroPos == 12) && (peer_id[12] == (byte)97) && (peer_id[13] == (byte)97)) { return "Experimental 3.2.1b2"; } if ((iFirstNonZeroPos == 12) && (peer_id[12] == (byte)0) && (peer_id[13] == (byte)0)) { return "Experimental 3.1"; } if (iFirstNonZeroPos == 12) return "Mainline"; return null; } private static String decodeBitSpiritClient(String peer_id, byte[] peer_id_bytes) { if (!peer_id.substring(2, 4).equals("BS")) {return null;} String version = BTPeerIDByteDecoderUtils.decodeNumericValueOfByte(peer_id_bytes[1]); if ("0".equals(version)) {version = "1";} return "BitSpirit v" + version; } private static String decodeBitCometClient(String peer_id, byte[] peer_id_bytes) { String mod_name = null; if (peer_id.startsWith("exbc")) {mod_name = "";} else if (peer_id.startsWith("FUTB")) {mod_name = "(Solidox Mod) ";} else if (peer_id.startsWith("xUTB")) {mod_name = "(Mod 2) ";} else {return null;} boolean is_bitlord = (peer_id.substring(6, 10).equals("LORD")); /** * Older versions of BitLord are of the form x.yy, whereas new versions (1 and onwards), * are of the form x.y. BitComet is of the form x.yy. */ String client_name = (is_bitlord) ? "BitLord " : "BitComet "; String maj_version = BTPeerIDByteDecoderUtils.decodeNumericValueOfByte(peer_id_bytes[4]); int min_version_length = (is_bitlord && !maj_version.equals("0")) ? 1 : 2; return client_name + mod_name + maj_version + "." + BTPeerIDByteDecoderUtils.decodeNumericValueOfByte(peer_id_bytes[5], min_version_length); } protected static String getPrintablePeerID(byte[] peer_id) { return getPrintablePeerID(peer_id, '-'); } protected static String getPrintablePeerID(byte[] peer_id, char fallback_char) { String sPeerID = ""; byte[] peerID = new byte[ peer_id.length ]; System.arraycopy( peer_id, 0, peerID, 0, peer_id.length ); try { for (int i = 0; i < peerID.length; i++) { int b = (0xFF & peerID[i]); if (b < 32 || b > 127) peerID[i] = (byte)fallback_char; } sPeerID = new String(peerID, Constants.BYTE_ENCODING); } catch (UnsupportedEncodingException ignore) {} catch (Throwable e) {} return( sPeerID ); } private static String makePeerIDReadableAndUsable(byte[] peer_id) { boolean as_ascii = true; for (int i=0; i<peer_id.length; i++) { int b = 0xFF & peer_id[i]; if (b < 32 || b > 127 || b == 10 || b == 9 || b==13) { as_ascii = false; break; } } if (as_ascii) { try {return new String(peer_id, Constants.BYTE_ENCODING);} catch (UnsupportedEncodingException uee) {return "";} } else {return ByteFormatter.encodeString(peer_id);} } static byte[] peerIDStringToBytes(String peer_id) throws Exception { if (peer_id.length() > 40) { peer_id = peer_id.replaceAll("[ ]", ""); } byte[] byte_peer_id = null; if (peer_id.length() == 40) { byte_peer_id = ByteFormatter.decodeString(peer_id); String readable_peer_id = makePeerIDReadableAndUsable(byte_peer_id); if (!peer_id.equals(readable_peer_id)) { throw new RuntimeException("Use alternative format for peer ID - from " + peer_id + " to " + readable_peer_id); } } else if (peer_id.length() == 20) { byte_peer_id = peer_id.getBytes(Constants.BYTE_ENCODING); } else { throw new IllegalArgumentException(peer_id); } return byte_peer_id; } private static void assertDecode(String client_result, String peer_id) throws Exception { assertDecode(client_result, peerIDStringToBytes(peer_id)); } private static void assertDecode(String client_result, byte[] peer_id) throws Exception { String peer_id_as_string = getPrintablePeerID(peer_id, '*'); System.out.println(" Peer ID: " + peer_id_as_string + " Client: " + client_result); // Do not log any clients. String decoded_result = decode(peer_id); if (client_result.equals(decoded_result)) {return;} throw new RuntimeException("assertion failure - expected \"" + client_result + "\", got \"" + decoded_result + "\": " + peer_id_as_string); } public static void main(String[] args) throws Exception { client_logging_allowed = false; // final String FAKE = MessageText.getString("PeerSocket.fake_client"); // final String UNKNOWN = MessageText.getString("PeerSocket.unknown"); // final String BAD_PEER_ID = MessageText.getString("PeerSocket.bad_peer_id"); System.out.println("Testing AZ style clients..."); assertDecode("Ares 2.0.5.3", "-AG2053-Em6o1EmvwLtD"); assertDecode("Ares 1.6.7.0", "-AR1670-3Ql6wM3hgtCc"); assertDecode("Artemis 2.5.2.0", "-AT2520-vEEt0wO6v0cr"); assertDecode("Azureus 2.2.0.0", "-AZ2200-6wfG2wk6wWLc"); assertDecode("BT Next Evolution 1.0.9", "-NE1090002IKyMn4g7Ko"); assertDecode("BitRocket 0.3(32)", "-BR0332-!XVceSn(*KIl"); assertDecode("Mainline 6.0 Beta", "2D555436 3030422D A78DC290 C3F7BDE0 15EC3CC7"); assertDecode("FlashGet 1.80", "2D464730 31383075 F8005782 1359D64B B3DFD265"); assertDecode("GetRight 6.3", "-GR6300-13s3iFKmbArc"); assertDecode("Halite 0.2.9", "-HL0290-xUO*9ugvENUE"); assertDecode("KTorrent 1.1 RC1", "-KT11R1-693649213030"); assertDecode("KTorrent 3.0", "2D4B543330302D006A7139727958377731756A4B"); assertDecode("libTorrent (Rakshasa) 0.11.2 / rTorrent*", "2D6C74304232302D0D739B93E6BE21FEBB557B20"); assertDecode("libtorrent (Rasterbar) 0.13.0", "-LT0D00-eZ0PwaDDr-~v"); // The latest version at time of writing is v0.12, but I'll assume this is valid. assertDecode("linkage 0.1.4", "-LK0140-ATIV~nbEQAMr"); assertDecode("LimeWire", "2D4C57303030312D31E0B3A0B46F7D4E954F4103"); assertDecode("Lphant 3.02", "2D4C5030 3330322D 00383336 35363935 37373030"); assertDecode("Shareaza 2.1.3.2", "2D535A323133322D000000000000000000000000"); assertDecode("SymTorrent 1.17", "-ST0117-01234567890!"); assertDecode("Transmission 0.6", "-TR0006-01234567890!"); assertDecode("Transmission 0.72 (Dev)", "-TR072Z-zihst5yvg22f"); assertDecode("Transmission 0.72", "-TR0072-8vd6hrmp04an"); assertDecode("TuoTu 2.1.0", "-TT210w-dq!nWf~Qcext"); assertDecode("\u00B5Torrent 1.7.0 Beta", "2D555431 3730422D 92844644 1DB0A094 A01C01E5"); assertDecode("\u54c7\u560E (Vagaa) 2.6.4.4", "2D5647323634342D4FD62CDA69E235717E3BB94B"); assertDecode("Wyzo 0.3.0.0", "-WY0300-6huHF5Pr7Vde"); assertDecode("CacheLogic 25.1-26", "-PC251Q-6huHF5Pr7Vde"); System.out.println(); // Shadow style clients. System.out.println("Testing Shadow style clients..."); assertDecode("ABC", "A--------YMyoBPXYy2L"); // Seen this quite a bit - not sure that it is ABC, but I guess we should default to that... assertDecode("ABC 2.6.9", "413236392D2D2D2D345077199FAEC4A673BECA01"); assertDecode("ABC 3.1", "A310--001v5Gysr4NxNK"); assertDecode("BitTornado 0.3.12", "T03C-----6tYolxhVUFS"); assertDecode("BitTornado 0.3.18", "T03I--008gY6iB6Aq27C"); assertDecode("BitTornado 0.3.9", "T0390----5uL5NvjBe2z"); assertDecode("Tribler 1", "R100--003hR6s07XWcov"); // Seen recently - is this really Tribler? assertDecode("Tribler 3.7", "R37---003uApHy851-Pq"); System.out.println(); // Simple substring style clients. System.out.println("Testing simple substring clients..."); assertDecode("Azureus 1", "417A7572 65757300 00000000 000000A0 76F0AEF7"); assertDecode("Azureus 2.0.3.2", "2D2D2D2D2D417A757265757354694E7A2A6454A7"); assertDecode("G3 Torrent", "2D473341 6E6F6E79 6D6F7573 70E8D9CB 30250AD4"); assertDecode("Hurricane Electric", "6172636C696768742E68652EA5860C157A5ADC35"); assertDecode("Pando", "Pando-6B511B691CAC2E"); // Seen recently, have they changed peer ID format? assertDecode("\u00B5Torrent 1.7.0 RC", "2D55543137302D00AF8BC5ACCC4631481EB3EB60"); System.out.println(); // Version substring style clients. System.out.println("Testing versioned substring clients..."); assertDecode("Bitlet 0.1", "4269744C657430319AEA4E02A09E318D70CCF47D"); assertDecode("BitsOnWheels", "-BOWP05-EPICNZOGQPHP"); // Seen in the wild - no idea what version that's meant to be - a pre-release? assertDecode("Burst! 1.1.3", "Mbrst1-1-32e3c394b43"); assertDecode("Opera (Build 7685)", "OP7685f2c1495b1680bf"); assertDecode("Opera (Build 10063)", "O100634008270e29150a"); assertDecode("Rufus 0.6.9", "00455253 416E6F6E 796D6F75 7382BE42 75024AE3"); assertDecode("BitTorrent DNA 1.0", "444E413031303030DD01C9B2DA689E6E02803E91"); assertDecode("BTuga Revolution 2.1", "BTM21abcdefghijklmno"); assertDecode("AllPeers 0.70rc30", "4150302E3730726333302D3E3EB87B31F241DBFE"); // AP0.70rc30->>-{1-A--]" assertDecode("External Webseed", "45787420EC7CC30033D7801FEEB713FBB0557AC4"); assertDecode("QVOD (Build 0054)", "QVOD00541234567890AB"); // Based on description on wiki.theory.org. assertDecode("Top-BT 1.0.0", "TB100----abcdefghijk"); System.out.println(); // BitComet/Lord/Spirit System.out.println("Testing BitComet/Lord/Spirit clients..."); assertDecode("BitComet 0.56", "6578626300387A4463102D6E9AD6723B339F35A9"); assertDecode("BitLord 0.56", "6578626300384C4F52443200048ECED57BD71028"); // assertDecode("BitSpirit? (" + BAD_PEER_ID + ")", "4D342D302D322D2D6898D9D0CAF25E4555445030"); assertDecode("BitSpirit v2", "000242539B7ED3E058A8384AA748485454504254"); assertDecode("BitSpirit v3", "00034253 07248896 44C59530 8A5FF2CA 55445030"); System.out.println(); // Mainline style clients. System.out.println("Testing new mainline style clients..."); assertDecode("Mainline 5.0.7", "M5-0-7--9aa757efd5be"); assertDecode("Amazon AWS S3", "S3-1-0-0--0123456789"); // Not currently decoded as mainline style... System.out.println(); // Various specialised clients. System.out.println("Testing various specialised clients..."); assertDecode("Mainline", "0000000000000000000000004C53441933104277"); // assertDecode(UNKNOWN + " [" + FAKE + ": ZipTorrent 1.6.0.0]", "-ZT1600-bLAdeY9rdjbe"); System.out.println(); // Unknown clients - may be random bytes. System.out.println("Testing unknown (random byte?) clients..."); // assertDecode(UNKNOWN + " [--------1}-/---A---<]", "0000000000000000317DA32F831FF041A515FE3C"); // assertDecode(UNKNOWN + " [------- -- ------@(]", "000000DF05020020100020200008000000004028"); // assertDecode(UNKNOWN + " [-----------D-y-I--aO]", "0000000000000000F106CE44F179A2498FAC614F"); // assertDecode(UNKNOWN + " [--c--_-5-\\----t-#---]", "E7F163BB0E5FCD35005C09A11BC274C42385A1A0"); System.out.println(); // Unknown AZ style clients. System.out.println("Testing unknown AZ style clients..."); String unknown_az; // unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"BD", "0.3.0.0"}); // assertDecode(unknown_az, "-BD0300-1SGiRZ8uWpWH"); // unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"wF", "2.2.0.0"}); // assertDecode(unknown_az, "2D7746323230302D9DFF296B56AFC2DF751C609C"); // unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"X1", "0.0.6.4"}); // assertDecode(unknown_az, "2D5831303036342D12FB8A5B954153A114267F1F"); // unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"bF", "2q00"}); // I made this one up. // assertDecode(unknown_az, "2D6246327130302D9DFF296B56AFC2DF751C609C"); System.out.println(); // Unknown Shadow style clients. System.out.println("Testing unknown Shadow style clients..."); String unknown_shadow; // unknown_shadow = MessageText.getString("PeerSocket.unknown_shadow_style", new String[]{"B", "1.2"}); // assertDecode(unknown_shadow, "B12------xgTofhetSVQ"); System.out.println(); // TODO //assertDecode("KTorrent 2.2", "-KT22B1-695754334315"); // We could use the B1 information... //assertDecode("KTorrent 2.1.4", "-KT2140-584815613993"); // Currently shows as 2.1. //assertDecode("", "C8F2D9CD3A90455354426578626300362D2D2D92"); // Looks like a BitLord client - ESTBexbc? //assertDecode("", "303030302D2D0000005433585859684B59584C72"); // Seen in the wild, appears to be a modified version of Azureus 2.5.0.0 (that's what was in the AZMP handshake)? //assertDecode("", "B5546F7272656E742F3330323520202020202020"); System.out.println("Done."); } }