├── .npmignore ├── .gitignore ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── CHANGELOG.md ├── LICENSE ├── package.json ├── README.md ├── lib └── utils.js ├── test └── basic.js └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | node: 15 | - '14' 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: actions/setup-node@v5 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0](https://github.com/webtorrent/bittorrent-peerid/compare/v1.3.5...v2.0.0) (2022-12-05) 2 | 3 | 4 | * Merge pull request #43 from ThaUnknown/esm ([14e51df](https://github.com/webtorrent/bittorrent-peerid/commit/14e51df6172e5629f072b72772285806c0fbb10b)), closes [#43](https://github.com/webtorrent/bittorrent-peerid/issues/43) 5 | 6 | 7 | ### BREAKING CHANGES 8 | 9 | * ESM only 10 | 11 | feat: esm 12 | 13 | ## [1.3.5](https://github.com/webtorrent/bittorrent-peerid/compare/v1.3.4...v1.3.5) (2022-12-04) 14 | 15 | ## [1.3.4](https://github.com/webtorrent/bittorrent-peerid/compare/v1.3.3...v1.3.4) (2021-07-23) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * add semantic release, switch CI ([ecd7966](https://github.com/webtorrent/bittorrent-peerid/commit/ecd7966cd4896f4494119f20bf048e69f7c472c7)) 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v5 15 | with: 16 | persist-credentials: false 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v5 19 | with: 20 | node-version: 22 21 | - name: Cache 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-npm- 28 | - name: Install dependencies 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | run: npm i 32 | - name: Release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Travis Fischer and WebTorrent, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bittorrent-peerid", 3 | "description": "Map a BitTorrent peer ID to a human-readable client name and version", 4 | "version": "2.0.0", 5 | "author": { 6 | "name": "WebTorrent LLC", 7 | "email": "feross@webtorrent.io", 8 | "url": "https://webtorrent.io" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/webtorrent/bittorrent-peerid/issues" 12 | }, 13 | "type": "module", 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "@webtorrent/semantic-release-config": "1.0.10", 17 | "semantic-release": "24.2.9", 18 | "standard": "*", 19 | "tape": "5.9.0" 20 | }, 21 | "keywords": [ 22 | ".torrent", 23 | "bittorrent", 24 | "peer", 25 | "peer-to-peer", 26 | "torrent", 27 | "webtorrent" 28 | ], 29 | "license": "MIT", 30 | "engines": { 31 | "node": ">=12.20.0" 32 | }, 33 | "exports": { 34 | "import": "./index.js" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/webtorrent/bittorrent-peerid.git" 39 | }, 40 | "scripts": { 41 | "test": "standard && tape test/*.js" 42 | }, 43 | "funding": [ 44 | { 45 | "type": "github", 46 | "url": "https://github.com/sponsors/feross" 47 | }, 48 | { 49 | "type": "patreon", 50 | "url": "https://www.patreon.com/feross" 51 | }, 52 | { 53 | "type": "consulting", 54 | "url": "https://feross.org/support" 55 | } 56 | ], 57 | "renovate": { 58 | "extends": [ 59 | "github>webtorrent/renovate-config" 60 | ], 61 | "lockFileMaintenance": { 62 | "enabled": false 63 | } 64 | }, 65 | "release": { 66 | "extends": "@webtorrent/semantic-release-config" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bittorrent-peerid [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [ci-image]: https://github.com/webtorrent/bittorrent-peerid/actions/workflows/ci.yml/badge.svg 4 | [ci-url]: https://github.com/webtorrent/bittorrent-peerid/actions/workflows/ci.yml 5 | [npm-image]: https://img.shields.io/npm/v/bittorrent-peerid.svg 6 | [npm-url]: https://npmjs.org/package/bittorrent-peerid 7 | [downloads-image]: https://img.shields.io/npm/dm/bittorrent-peerid.svg 8 | [downloads-url]: https://npmjs.org/package/bittorrent-peerid 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | ### Map a BitTorrent peer ID to a human-readable client name and version 13 | 14 | Also works in the browser with [browserify](http://browserify.org/)! 15 | 16 | This module is used by [WebTorrent](https://webtorrent.io). 17 | 18 | ## install 19 | 20 | ``` 21 | npm install bittorrent-peerid 22 | ``` 23 | 24 | ## usage 25 | 26 | ```js 27 | import peerid from 'bittorrent-peerid' 28 | const parsed = peerid('-AZ2200-6wfG2wk6wWLc') 29 | 30 | console.log(parsed.client, parsed.version) 31 | ``` 32 | 33 | The `parsed` peerid object looks like this: 34 | 35 | ```js 36 | { 37 | client: 'Vuze', 38 | version: '2.2.0.0' 39 | } 40 | ``` 41 | 42 | bittorrent-peerid can parse peer ids encoded in the following formats: 43 | * a 20-byte Buffer 44 | * a 40-character hex string 45 | * an arbitrarily-sized human-readable utf8 string (must decode to a 20-byte Buffer) 46 | 47 | If an unknown peer id is passed in, the returned client will be `unknown`. 48 | 49 | ## todo 50 | 51 | * ~~Support known Azureus-style clients.~~ 52 | * ~~Support known Shadow-style clients.~~ 53 | * ~~Support known Mainline-style clients.~~ 54 | * ~~Support known Custom-style clients.~~ 55 | * ~~Recognize BitComet/Lord/Spirit spoofing.~~ 56 | * Full support for client version parsing. 57 | * Full support for customized client version schemes. 58 | * Support unknown clients that conform to either the Azureus or Shadow-style conventions. 59 | 60 | ## credit 61 | 62 | This module is based heavily on the BTPeerIDByteDecoderDefinitions class from [Azureus](http://sourceforge.net/projects/azureus/) (Vuze). Related resources include: 63 | * [BitTorrent Specification](http://wiki.theory.org/BitTorrentSpecification) 64 | * [Transmission](http://transmission.m0k.org/trac/browser/trunk/libtransmission/clients.c) 65 | * [g3peerid](http://rufus.cvs.sourceforge.net/rufus/Rufus/g3peerid.py?view=log) 66 | * [Shareaza](http://shareaza.svn.sourceforge.net/viewvc/shareaza/trunk/shareaza/BTClient.cpp?view=markup) 67 | * [libtorrent](http://libtorrent.rakshasa.no/browser/trunk/libtorrent/src/torrent/peer/client_list.cc) 68 | 69 | ## license 70 | 71 | MIT. Copyright (c) Travis Fischer and [WebTorrent, LLC](https://webtorrent.io). 72 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | export function isAzStyle (peerId) { 2 | if (peerId.charAt(0) !== '-') return false 3 | if (peerId.charAt(7) === '-') return true 4 | 5 | /** 6 | * Hack for FlashGet - it doesn't use the trailing dash. 7 | * Also, LH-ABC has strayed into "forgetting about the delimiter" territory. 8 | * 9 | * In fact, the code to generate a peer ID for LH-ABC is based on BitTornado's, 10 | * yet tries to give an Az style peer ID... oh dear. 11 | * 12 | * BT Next Evolution seems to be in the same boat as well. 13 | * 14 | * KTorrent 3 appears to use a dash rather than a final character. 15 | */ 16 | if (['FG', 'LH', 'NE', 'KT', 'SP'].includes(peerId.substring(1, 3))) return true 17 | 18 | return false 19 | } 20 | 21 | /** 22 | * Checking whether a peer ID is Shadow style or not is a bit tricky. 23 | * 24 | * The BitTornado peer ID convention code is explained here: 25 | * http://forums.degreez.net/viewtopic.php?t=7070 26 | * 27 | * The main thing we are interested in is the first six characters. 28 | * Although the other characters are base64 characters, there's no 29 | * guarantee that other clients which follow that style will follow 30 | * that convention (though the fact that some of these clients use 31 | * BitTornado in the core does blur the lines a bit between what is 32 | * "style" and what is just common across clients). 33 | * 34 | * So if we base it on the version number information, there's another 35 | * problem - there isn't the use of absolute delimiters (no fixed dash 36 | * character, for example). 37 | * 38 | * There are various things we can do to determine how likely the peer 39 | * ID is to be of that style, but for now, I'll keep it to a relatively 40 | * simple check. 41 | * 42 | * We'll assume that no client uses the fifth version digit, so we'll 43 | * expect a dash. We'll also assume that no client has reached version 10 44 | * yet, so we expect the first two characters to be "letter,digit". 45 | * 46 | * We've seen some clients which don't appear to contain any version 47 | * information, so we need to allow for that. 48 | */ 49 | export function isShadowStyle (peerId) { 50 | if (peerId.charAt(5) !== '-') return false 51 | if (!isLetter(peerId.charAt(0))) return false 52 | if (!(isDigit(peerId.charAt(1)) || peerId.charAt(1) === '-')) return false 53 | 54 | // Find where the version number string ends. 55 | let lastVersionNumberIndex = 4 56 | for (; lastVersionNumberIndex > 0; lastVersionNumberIndex--) { 57 | if (peerId.charAt(lastVersionNumberIndex) !== '-') break 58 | } 59 | 60 | // For each digit in the version string, check if it is a valid version identifier. 61 | for (let i = 1; i <= lastVersionNumberIndex; i++) { 62 | const c = peerId.charAt(i) 63 | if (c === '-') return false 64 | if (isAlphaNumeric(c) === null) return false 65 | } 66 | 67 | return true 68 | } 69 | 70 | export function isMainlineStyle (peerId) { 71 | /** 72 | * One of the following styles will be used: 73 | * Mx-y-z-- 74 | * Mx-yy-z- 75 | */ 76 | return peerId.charAt(2) === '-' && peerId.charAt(7) === '-' && 77 | (peerId.charAt(4) === '-' || peerId.charAt(5) === '-') 78 | } 79 | 80 | export function isPossibleSpoofClient (peerId) { 81 | return peerId.endsWith('UDP0') || peerId.endsWith('HTTPBT') 82 | } 83 | 84 | export function getAzStyleVersionNumber (peerId, version) { 85 | if (typeof version === 'function') { 86 | return version(peerId) 87 | } 88 | return null 89 | } 90 | 91 | export function getShadowStyleVersionNumber (peerId) { 92 | // TODO 93 | return null 94 | } 95 | 96 | export function decodeBitSpiritClient (peerId, buffer) { 97 | if (peerId.substring(2, 4) !== 'BS') return null 98 | let version = `${buffer[1]}` 99 | if (version === '0') version = 1 100 | 101 | return { 102 | client: 'BitSpirit', 103 | version 104 | } 105 | } 106 | 107 | export function decodeBitCometClient (peerId, buffer) { 108 | let modName = '' 109 | if (peerId.startsWith('exbc')) modName = '' 110 | else if (peerId.startsWith('FUTB')) modName = '(Solidox Mod)' 111 | else if (peerId.startsWith('xUTB')) modName = '(Mod 2)' 112 | else return null 113 | 114 | const isBitlord = (peerId.substring(6, 10) === 'LORD') 115 | 116 | // Older versions of BitLord are of the form x.yy, whereas new versions (1 and onwards), 117 | // are of the form x.y. BitComet is of the form x.yy 118 | const clientName = (isBitlord) ? 'BitLord' : 'BitComet' 119 | const majVersion = decodeNumericValueOfByte(buffer[4]) 120 | const minVersionLength = (isBitlord && majVersion !== '0' ? 1 : 2) 121 | 122 | return { 123 | client: clientName + (modName ? ` ${modName}` : ''), 124 | version: `${majVersion}.${decodeNumericValueOfByte(buffer[5], minVersionLength)}` 125 | } 126 | } 127 | 128 | export function identifyAwkwardClient (peerId, buffer) { 129 | let firstNonZeroIndex = 20 130 | let i 131 | 132 | for (i = 0; i < 20; ++i) { 133 | if (buffer[i] > 0) { 134 | firstNonZeroIndex = i 135 | break 136 | } 137 | } 138 | 139 | // Shareaza check 140 | if (firstNonZeroIndex === 0) { 141 | let isShareaza = true 142 | for (i = 0; i < 16; ++i) { 143 | if (buffer[i] === 0) { 144 | isShareaza = false 145 | break 146 | } 147 | } 148 | 149 | if (isShareaza) { 150 | for (i = 16; i < 20; ++i) { 151 | if (buffer[i] !== (buffer[i % 16] ^ buffer[15 - (i % 16)])) { 152 | isShareaza = false 153 | break 154 | } 155 | } 156 | 157 | if (isShareaza) return { client: 'Shareaza' } 158 | } 159 | } 160 | 161 | if (firstNonZeroIndex === 9 && buffer[9] === 3 && buffer[10] === 3 && buffer[11] === 3) { return { client: 'I2PSnark' } } 162 | 163 | if (firstNonZeroIndex === 12 && buffer[12] === 97 && buffer[13] === 97) { return { client: 'Experimental', version: '3.2.1b2' } } 164 | 165 | if (firstNonZeroIndex === 12 && buffer[12] === 0 && buffer[13] === 0) { return { client: 'Experimental', version: '3.1' } } 166 | 167 | if (firstNonZeroIndex === 12) { return { client: 'Mainline' } } 168 | 169 | return null 170 | } 171 | 172 | export function decodeNumericValueOfByte (b, minDigits = 0) { 173 | let result = `${b & 0xff}` 174 | while (result.length < minDigits) { result = `0${result}` } 175 | return result 176 | } 177 | 178 | // 179 | // Private helper functions for the public utility functions 180 | // 181 | 182 | function isDigit (s) { 183 | const code = s.charCodeAt(0) 184 | return code >= '0'.charCodeAt(0) && code <= '9'.charCodeAt(0) 185 | } 186 | 187 | function isLetter (s) { 188 | const code = s.toLowerCase().charCodeAt(0) 189 | return code >= 'a'.charCodeAt(0) && code <= 'z'.charCodeAt(0) 190 | } 191 | 192 | function isAlphaNumeric (s) { 193 | return isDigit(s) || isLetter(s) || s === '.' 194 | } 195 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | // TODO: test versions 2 | // TODO: test clients with custom versioning schemes 3 | 4 | import test from 'tape' 5 | import peerid from '../index.js' 6 | 7 | test('basic clients from utf8 strings', t => { 8 | t.equal(peerid('-AG2053-Em6o1EmvwLtD').client, 'Ares') 9 | t.equal(peerid('-AZ2200-6wfG2wk6wWLc').client, 'Vuze') 10 | t.equal(peerid('-TR0072-8vd6hrmp04an').client, 'Transmission') 11 | t.equal(peerid('-WY0300-6huHF5Pr7Vde').client, 'FireTorrent') 12 | t.equal(peerid('-PC251Q-6huHF5Pr7Vde').client, 'CacheLogic') 13 | t.end() 14 | }) 15 | 16 | test('basic clients from hex strings', t => { 17 | t.equal(peerid('2D535A323133322D000000000000000000000000').client, 'Shareaza') 18 | t.equal(peerid('2D5554313730422D928446441DB0A094A01C01E5').client, '\u00B5Torrent') 19 | t.equal(peerid('2D4C57303030312D31E0B3A0B46F7D4E954F4103').client, 'LimeWire') 20 | t.equal(peerid('2D4C50303330322D003833363536393537373030').client, 'Lphant') 21 | t.end() 22 | }) 23 | 24 | test('basic clients from Buffers', t => { 25 | t.equal(peerid(Buffer.from('-AG2053-Em6o1EmvwLtD', 'utf8')).client, 'Ares') 26 | t.equal(peerid(Buffer.from('-AZ2200-6wfG2wk6wWLc', 'utf8')).client, 'Vuze') 27 | t.equal(peerid(Buffer.from('-TR0072-8vd6hrmp04an', 'utf8')).client, 'Transmission') 28 | t.equal(peerid(Buffer.from('-WY0300-6huHF5Pr7Vde', 'utf8')).client, 'FireTorrent') 29 | t.equal(peerid(Buffer.from('-PC251Q-6huHF5Pr7Vde', 'utf8')).client, 'CacheLogic') 30 | t.end() 31 | }) 32 | 33 | test('Azureus-style clients', t => { 34 | t.equal(peerid('-AG2053-Em6o1EmvwLtD').client, 'Ares') 35 | t.equal(peerid('-AR1670-3Ql6wM3hgtCc').client, 'Ares') 36 | t.equal(peerid('-AT2520-vEEt0wO6v0cr').client, 'Artemis') 37 | t.equal(peerid('-AZ2200-6wfG2wk6wWLc').client, 'Vuze') 38 | t.equal(peerid('-NE1090002IKyMn4g7Ko').client, 'BT Next Evolution') 39 | t.equal(peerid('-BR0332-!XVceSn(*KIl').client, 'BitRocket') 40 | t.equal(peerid('2D46473031383075F80057821359D64BB3DFD265').client, 'FlashGet') 41 | t.equal(peerid('-GR6300-13s3iFKmbArc').client, 'GetRight') 42 | t.equal(peerid('-HL0290-xUO*9ugvENUE').client, 'Halite') 43 | t.equal(peerid('-KT11R1-693649213030').client, 'KTorrent') 44 | t.equal(peerid('2D4B543330302D006A7139727958377731756A4B').client, 'KTorrent') 45 | t.equal(peerid('2D6C74304232302D0D739B93E6BE21FEBB557B20').client, 'libTorrent (Rakshasa) / rTorrent*') 46 | t.equal(peerid('-LT0D00-eZ0PwaDDr-~v').client, 'libtorrent (Rasterbar)') 47 | t.equal(peerid('-LK0140-ATIV~nbEQAMr').client, 'linkage') 48 | t.equal(peerid('2D4C57303030312D31E0B3A0B46F7D4E954F4103').client, 'LimeWire') 49 | t.equal(peerid('2D4C50303330322D003833363536393537373030').client, 'Lphant') 50 | t.equal(peerid('2D535A323133322D000000000000000000000000').client, 'Shareaza') 51 | t.equal(peerid('-ST0117-01234567890!').client, 'SymTorrent') 52 | t.equal(peerid('-TR0006-01234567890!').client, 'Transmission') 53 | t.equal(peerid('-TR072Z-zihst5yvg22f').client, 'Transmission') 54 | t.equal(peerid('-TR0072-8vd6hrmp04an').client, 'Transmission') 55 | t.equal(peerid('-TT210w-dq!nWf~Qcext').client, 'TuoTu') 56 | t.equal(peerid('2D5554313730422D928446441DB0A094A01C01E5').client, '\u00B5Torrent') 57 | t.equal(peerid('2D5647323634342D4FD62CDA69E235717E3BB94B').client, '\u54c7\u560E (Vagaa)') 58 | t.equal(peerid('-WY0300-6huHF5Pr7Vde').client, 'FireTorrent') 59 | t.equal(peerid('-PC251Q-6huHF5Pr7Vde').client, 'CacheLogic') 60 | t.equal(peerid('-KG2450-BDEw8OM14Hk6').client, 'KGet') 61 | t.end() 62 | }) 63 | 64 | test('Shadow-style clients', t => { 65 | t.equal(peerid('A--------YMyoBPXYy2L').client, 'ABC') 66 | t.equal(peerid('413236392D2D2D2D345077199FAEC4A673BECA01').client, 'ABC') 67 | t.equal(peerid('A310--001v5Gysr4NxNK').client, 'ABC') 68 | t.equal(peerid('T03C-----6tYolxhVUFS').client, 'BitTornado') 69 | t.equal(peerid('T03I--008gY6iB6Aq27C').client, 'BitTornado') 70 | t.equal(peerid('T0390----5uL5NvjBe2z').client, 'BitTornado') 71 | t.equal(peerid('R100--003hR6s07XWcov').client, 'Tribler') 72 | t.equal(peerid('R37---003uApHy851-Pq').client, 'Tribler') 73 | t.end() 74 | }) 75 | 76 | test('Simple-style clients', t => { 77 | t.equal(peerid('417A75726575730000000000000000A076F0AEF7').client, 'Azureus') 78 | t.equal(peerid('2D2D2D2D2D417A757265757354694E7A2A6454A7').client, 'Azureus') 79 | t.equal(peerid('2D4733416E6F6E796D6F757370E8D9CB30250AD4').client, 'G3 Torrent') 80 | t.equal(peerid('6172636C696768742E68652EA5860C157A5ADC35').client, 'Hurricane Electric') 81 | t.equal(peerid('Pando-6B511B691CAC2E').client, 'Pando') 82 | t.equal(peerid('2D55543137302D00AF8BC5ACCC4631481EB3EB60').client, '\u00B5Torrent') 83 | t.end() 84 | }) 85 | 86 | test('Mainline-style clients', t => { 87 | t.equal(peerid('M5-0-7--9aa757efd5be').client, 'Mainline') 88 | t.equal(peerid('0000000000000000000000004C53441933104277').client, 'Mainline') 89 | t.equal(peerid('S3-1-0-0--0123456789').client, 'Amazon AWS S3') 90 | t.end() 91 | }) 92 | 93 | test('Version substring-style clients', t => { 94 | t.equal(peerid('4269744C657430319AEA4E02A09E318D70CCF47D').client, 'Bitlet') 95 | t.equal(peerid('-BOWP05-EPICNZOGQPHP').client, 'BitsOnWheels') 96 | t.equal(peerid('Mbrst1-1-32e3c394b43').client, 'Burst!') 97 | t.equal(peerid('OP7685f2c1495b1680bf').client, 'Opera') 98 | t.equal(peerid('O100634008270e29150a').client, 'Opera') 99 | t.equal(peerid('00455253416E6F6E796D6F757382BE4275024AE3').client, 'Rufus') 100 | t.equal(peerid('444E413031303030DD01C9B2DA689E6E02803E91').client, 'BitTorrent DNA') 101 | t.equal(peerid('BTM21abcdefghijklmno').client, 'BTuga Revolution') 102 | t.equal(peerid('4150302E3730726333302D3E3EB87B31F241DBFE').client, 'AllPeers') 103 | t.equal(peerid('45787420EC7CC30033D7801FEEB713FBB0557AC4').client, 'External Webseed') 104 | t.equal(peerid('QVOD00541234567890AB').client, 'QVOD') 105 | t.equal(peerid('TB100----abcdefghijk').client, 'Top-BT') 106 | t.end() 107 | }) 108 | 109 | test('BitComet/Lord/Spirit', t => { 110 | t.equal(peerid('6578626300387A4463102D6E9AD6723B339F35A9').client, 'BitComet') 111 | t.equal(peerid('6578626300384C4F52443200048ECED57BD71028').client, 'BitLord') 112 | t.equal(peerid('4D342D302D322D2D6898D9D0CAF25E4555445030').client, 'BitSpirit?') 113 | t.equal(peerid('000242539B7ED3E058A8384AA748485454504254').client, 'BitSpirit') 114 | t.equal(peerid('000342530724889644C595308A5FF2CA55445030').client, 'BitSpirit') 115 | t.end() 116 | }) 117 | 118 | test('Misc clients', t => { 119 | t.equal(peerid('TIX0137-i6i6f0i5d5b7').client, 'Tixati') 120 | t.equal(peerid('2D464C3039C6F22D5F436863327A6D792E283867').client, 'folx') 121 | t.equal(peerid('-KT22B1-695754334315').client, 'KTorrent') 122 | t.equal(peerid('-KT2140-584815613993').client, 'KTorrent') 123 | t.equal(peerid('2D554D3135313130C964BE6F15CA71EF02AF2DD7').client, '\u00B5Torrent Mac') 124 | t.equal(peerid('2D4D47314372302D3234705F6436000055673362').client, 'MediaGet') 125 | t.equal(peerid('-#@0000-Em6o1EmvwLtD').client, 'Invalid PeerID') 126 | t.equal(peerid('2D4D47323111302D3234705F6436706E55673362').client, 'MediaGet') 127 | t.equal(peerid('-AN2171-nr17R1h19O7n').client, 'Ares') 128 | t.equal(peerid('2D55543334302D000971FDE48C3688D2023506FC').client, '\u00B5Torrent') 129 | t.end() 130 | }) 131 | 132 | test('Unknown clients', t => { 133 | t.equal(peerid('B5546F7272656E742F3330323520202020202020').client, 'unknown') 134 | t.equal(peerid('0000000000000000317DA32F831FF041A515FE3C').client, 'unknown') 135 | t.equal(peerid('000000DF05020020100020200008000000004028').client, 'unknown') 136 | t.equal(peerid('0000000000000000F106CE44F179A2498FAC614F').client, 'unknown') 137 | t.equal(peerid('E7F163BB0E5FCD35005C09A11BC274C42385A1A0').client, 'unknown') 138 | t.equal(peerid('2D464435315DC72D37426772646B4C3850434239').client, 'unknown') 139 | t.equal(peerid('2D4249313730302D66466D324E356B5848335068').client, 'unknown') 140 | t.end() 141 | }) 142 | 143 | test('WebTorrent', t => { 144 | const parsed = peerid('-WW0000-Em6o1EmvwLtD') 145 | t.equal(parsed.client, 'WebTorrent') 146 | t.equal(parsed.version, '0.0') 147 | t.equal(peerid('-WW0100-Em6o1EmvwLtD').version, '1.0') 148 | t.equal(peerid('-WW1000-Em6o1EmvwLtD').version, '10.0') 149 | t.equal(peerid('-WW0001-Em6o1EmvwLtD').version, '0.1') 150 | t.equal(peerid('-WW0010-Em6o1EmvwLtD').version, '0.10') 151 | t.equal(peerid('-WW0011-Em6o1EmvwLtD').version, '0.11') 152 | t.equal(peerid('-WW1011-Em6o1EmvwLtD').version, '10.11') 153 | t.equal(peerid('-WW1111-Em6o1EmvwLtD').version, '11.11') 154 | t.end() 155 | }) 156 | 157 | test('WebTorrent Desktop', t => { 158 | const parsed = peerid('-WD0007-Em6o1EmvwLtD') 159 | t.equal(parsed.client, 'WebTorrent Desktop') 160 | t.equal(parsed.version, '0.7') 161 | t.end() 162 | }) 163 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! bittorrent-peerid. MIT License. WebTorrent LLC */ 2 | import { decodeBitCometClient, decodeBitSpiritClient, getAzStyleVersionNumber, identifyAwkwardClient, isAzStyle, isMainlineStyle, isPossibleSpoofClient, isShadowStyle } from './lib/utils.js' 3 | 4 | /** 5 | * Parses and returns the client type and version of a bittorrent peer id. 6 | * Throws an exception if the peer id is invalid. 7 | * 8 | * @param {Buffer|string} peerId (as Buffer or hex/utf8 string) 9 | */ 10 | export default (peerId) => { 11 | let buffer 12 | 13 | if (Buffer.isBuffer(peerId)) { 14 | buffer = peerId 15 | } else if (typeof peerId === 'string') { 16 | buffer = Buffer.from(peerId, 'utf8') 17 | 18 | // assume utf8 peerId, but if that's invalid, then try hex encoding 19 | if (buffer.length !== 20) { buffer = Buffer.from(peerId, 'hex') } 20 | } else { 21 | throw new Error(`Invalid peerId must be Buffer or hex string: ${peerId}`) 22 | } 23 | 24 | if (buffer.length !== 20) { 25 | throw new Error(`Invalid peerId length (hex buffer must be 20 bytes): ${peerId}`) 26 | } 27 | 28 | // overwrite original peerId string with guaranteed utf8 version 29 | peerId = buffer.toString('utf8') 30 | 31 | let client = null 32 | 33 | // If the client reuses parts of the peer ID of other peers, then try to determine this 34 | // first (before we misidentify the client). 35 | if (isPossibleSpoofClient(peerId)) { 36 | if ((client = decodeBitSpiritClient(peerId, buffer))) return client 37 | if ((client = decodeBitCometClient(peerId, buffer))) return client 38 | return { client: 'BitSpirit?' } 39 | } 40 | 41 | // See if the client uses Az style identification 42 | if (isAzStyle(peerId)) { 43 | if ((client = getAzStyleClientName(peerId))) { 44 | const version = getAzStyleClientVersion(client, peerId) 45 | 46 | // Hack for fake ZipTorrent clients - there seems to be some clients 47 | // which use the same identifier, but they aren't valid ZipTorrent clients 48 | if (client.startsWith('ZipTorrent') && peerId.startsWith('bLAde', 8)) { 49 | return { 50 | client: 'Unknown [Fake: ZipTorrent]', 51 | version 52 | } 53 | } 54 | 55 | // BitTorrent 6.0 Beta currently misidentifies itself 56 | if (client === '\u00B5Torrent' && version === '6.0 Beta') { 57 | return { 58 | client: 'Mainline', 59 | version: '6.0 Beta' 60 | } 61 | } 62 | 63 | // If it's the rakshasa libtorrent, then it's probably rTorrent 64 | if (client.startsWith('libTorrent (Rakshasa)')) { 65 | return { 66 | client: `${client} / rTorrent*`, 67 | version 68 | } 69 | } 70 | 71 | return { 72 | client, 73 | version 74 | } 75 | } 76 | } 77 | 78 | // See if the client uses Shadow style identification 79 | if (isShadowStyle(peerId)) { 80 | if ((client = getShadowStyleClientName(peerId))) { 81 | // TODO: handle shadow style client version numbers 82 | return { client } 83 | } 84 | } 85 | 86 | // See if the client uses Mainline style identification 87 | if (isMainlineStyle(peerId)) { 88 | if ((client = getMainlineStyleClientName(peerId))) { 89 | // TODO: handle mainline style client version numbers 90 | return { client } 91 | } 92 | } 93 | 94 | // Check for BitSpirit / BitComet disregarding spoof mode 95 | if ((client = decodeBitSpiritClient(peerId, buffer))) return client 96 | if ((client = decodeBitCometClient(peerId, buffer))) return client 97 | 98 | // See if the client identifies itself using a particular substring 99 | const data = getSimpleClient(peerId) 100 | if (data) { 101 | client = data.client 102 | 103 | // TODO: handle simple client version numbers 104 | return { 105 | client, 106 | version: data.version 107 | } 108 | } 109 | 110 | // See if client is known to be awkward / nonstandard 111 | if ((client = identifyAwkwardClient(peerId, buffer))) { 112 | return client 113 | } 114 | 115 | // TODO: handle unknown az-formatted and shadow-formatted clients 116 | return { client: 'unknown' } 117 | } 118 | 119 | // Az style two byte code identifiers to real client name 120 | const azStyleClients = {} 121 | const azStyleClientVersions = {} 122 | 123 | // Shadow's style one byte code identifiers to real client name 124 | const shadowStyleClients = {} 125 | const shadowStyleClientVersions = {} 126 | 127 | // Mainline's new style uses one byte code identifiers too 128 | const mainlineStyleClients = {} 129 | 130 | // Clients with completely custom naming schemes 131 | const customStyleClients = [] 132 | 133 | const VER_AZ_THREE_DIGITS = v => // "1.2.3" 134 | `${v[0]}.${v[1]}.${v[2]}` 135 | const VER_AZ_DELUGE = v => { 136 | const alphabet = 'ABCDE' 137 | if (isNaN(v[2])) { 138 | return `${v[0]}.${v[1]}.1${alphabet.indexOf(v[2])}` 139 | } 140 | return `${v[0]}.${v[1]}.${v[2]}` 141 | } 142 | const VER_AZ_THREE_DIGITS_PLUS_MNEMONIC = v => { 143 | // "1.2.3 [4]" 144 | let mnemonic = v[3] 145 | if (mnemonic === 'B') { 146 | mnemonic = 'Beta' 147 | } else if (mnemonic === 'A') { 148 | mnemonic = 'Alpha' 149 | } else { 150 | mnemonic = '' 151 | } 152 | return `${v[0]}.${v[1]}.${v[2]} ${mnemonic}` 153 | } 154 | const VER_AZ_FOUR_DIGITS = v => // "1.2.3.4" 155 | `${v[0]}.${v[1]}.${v[2]}.${v[3]}` 156 | const VER_AZ_TWO_MAJ_TWO_MIN = v => // "12.34" 157 | `${v[0] + v[1]}.${v[2]}${v[3]}` 158 | const VER_AZ_SKIP_FIRST_ONE_MAJ_TWO_MIN = v => // "2.34" 159 | `${v[1]}.${v[2]}${v[3]}` 160 | const VER_AZ_KTORRENT_STYLE = '1.2.3=[RD].4' 161 | const VER_AZ_TRANSMISSION_STYLE = v => { 162 | // "transmission" 163 | if (v[0] === '0' && v[1] === '0' && v[2] === '0') { 164 | return `0.${v[3]}` 165 | } else if (v[0] === '0' && v[1] === '0') { 166 | return `0.${v[2]}${v[3]}` 167 | } 168 | return `${v[0]}.${v[1]}${v[2]}${v[3] === 'Z' || v[3] === 'X' ? '+' : ''}` 169 | } 170 | const VER_AZ_WEBTORRENT_STYLE = v => { 171 | // "webtorrent" 172 | let version = '' 173 | if (v[0] === '0') { 174 | version += `${v[1]}.` 175 | } else { 176 | version += `${v[0]}${v[1]}.` 177 | } 178 | if (v[2] === '0') { 179 | version += v[3] 180 | } else { 181 | version += `${v[2]}${v[3]}` 182 | } 183 | return version 184 | } 185 | const VER_AZ_THREE_ALPHANUMERIC_DIGITS = '2.33.4' 186 | const VER_NONE = 'NO_VERSION' 187 | 188 | function addAzStyle (id, client, version = VER_AZ_FOUR_DIGITS) { 189 | azStyleClients[id] = client 190 | azStyleClientVersions[client] = version 191 | } 192 | 193 | function addShadowStyle (id, client, version = VER_AZ_THREE_DIGITS) { 194 | shadowStyleClients[id] = client 195 | shadowStyleClientVersions[client] = version 196 | } 197 | 198 | function addMainlineStyle (id, client) { 199 | mainlineStyleClients[id] = client 200 | } 201 | 202 | function addSimpleClient (client, version, id, position) { 203 | if (typeof id === 'number' || typeof id === 'undefined') { 204 | position = id 205 | id = version 206 | version = undefined 207 | } 208 | 209 | customStyleClients.push({ 210 | id, 211 | client, 212 | version, 213 | position: position || 0 214 | }) 215 | } 216 | 217 | function getAzStyleClientName (peerId) { 218 | return azStyleClients[peerId.substring(1, 3)] 219 | } 220 | 221 | function getShadowStyleClientName (peerId) { 222 | return shadowStyleClients[peerId.substring(0, 1)] 223 | } 224 | 225 | function getMainlineStyleClientName (peerId) { 226 | return mainlineStyleClients[peerId.substring(0, 1)] 227 | } 228 | 229 | function getSimpleClient (peerId) { 230 | for (let i = 0; i < customStyleClients.length; ++i) { 231 | const client = customStyleClients[i] 232 | 233 | if (peerId.startsWith(client.id, client.position)) { 234 | return client 235 | } 236 | } 237 | 238 | return null 239 | } 240 | 241 | function getAzStyleClientVersion (client, peerId) { 242 | const version = azStyleClientVersions[client] 243 | if (!version) return null 244 | 245 | return getAzStyleVersionNumber(peerId.substring(3, 7), version) 246 | } 247 | 248 | (() => { 249 | // add known clients alphabetically 250 | addAzStyle('A~', 'Ares', VER_AZ_THREE_DIGITS) 251 | addAzStyle('AG', 'Ares', VER_AZ_THREE_DIGITS) 252 | addAzStyle('AN', 'Ares', VER_AZ_FOUR_DIGITS) 253 | addAzStyle('AR', 'Ares')// Ares is more likely than ArcticTorrent 254 | addAzStyle('AV', 'Avicora') 255 | addAzStyle('AX', 'BitPump', VER_AZ_TWO_MAJ_TWO_MIN) 256 | addAzStyle('AT', 'Artemis') 257 | addAzStyle('AZ', 'Vuze', VER_AZ_FOUR_DIGITS) 258 | addAzStyle('BB', 'BitBuddy', '1.234') 259 | addAzStyle('BC', 'BitComet', VER_AZ_SKIP_FIRST_ONE_MAJ_TWO_MIN) 260 | addAzStyle('BE', 'BitTorrent SDK') 261 | addAzStyle('BF', 'BitFlu', VER_NONE) 262 | addAzStyle('BG', 'BTG', VER_AZ_FOUR_DIGITS) 263 | addAzStyle('bk', 'BitKitten (libtorrent)') 264 | addAzStyle('BR', 'BitRocket', '1.2(34)') 265 | addAzStyle('BS', 'BTSlave') 266 | addAzStyle('BT', 'BitTorrent', VER_AZ_THREE_DIGITS_PLUS_MNEMONIC) 267 | addAzStyle('BW', 'BitWombat') 268 | addAzStyle('BX', 'BittorrentX') 269 | addAzStyle('CB', 'Shareaza Plus') 270 | addAzStyle('CD', 'Enhanced CTorrent', VER_AZ_TWO_MAJ_TWO_MIN) 271 | addAzStyle('CT', 'CTorrent', '1.2.34') 272 | addAzStyle('DP', 'Propogate Data Client') 273 | addAzStyle('DE', 'Deluge', VER_AZ_DELUGE) 274 | addAzStyle('EB', 'EBit') 275 | addAzStyle('ES', 'Electric Sheep', VER_AZ_THREE_DIGITS) 276 | addAzStyle('FC', 'FileCroc') 277 | addAzStyle('FG', 'FlashGet', VER_AZ_SKIP_FIRST_ONE_MAJ_TWO_MIN) 278 | addAzStyle('FX', 'Freebox BitTorrent') 279 | addAzStyle('FT', 'FoxTorrent/RedSwoosh') 280 | addAzStyle('GR', 'GetRight', '1.2') 281 | addAzStyle('GS', 'GSTorrent')// TODO: Format is v"abcd" 282 | addAzStyle('HL', 'Halite', VER_AZ_THREE_DIGITS) 283 | addAzStyle('HN', 'Hydranode') 284 | addAzStyle('KG', 'KGet') 285 | addAzStyle('KT', 'KTorrent', VER_AZ_KTORRENT_STYLE) 286 | addAzStyle('LC', 'LeechCraft') 287 | addAzStyle('LH', 'LH-ABC') 288 | addAzStyle('LK', 'linkage', VER_AZ_THREE_DIGITS) 289 | addAzStyle('LP', 'Lphant', VER_AZ_TWO_MAJ_TWO_MIN) 290 | addAzStyle('LT', 'libtorrent (Rasterbar)', VER_AZ_THREE_ALPHANUMERIC_DIGITS) 291 | addAzStyle('lt', 'libTorrent (Rakshasa)', VER_AZ_THREE_ALPHANUMERIC_DIGITS) 292 | addAzStyle('LW', 'LimeWire', VER_NONE)// The "0001" bytes found after the LW commonly refers to the version of the BT protocol implemented. Documented here: http://www.limewire.org/wiki/index.php?title=BitTorrentRevision 293 | addAzStyle('MO', 'MonoTorrent') 294 | addAzStyle('MP', 'MooPolice', VER_AZ_THREE_DIGITS) 295 | addAzStyle('MR', 'Miro') 296 | addAzStyle('MT', 'MoonlightTorrent') 297 | addAzStyle('NE', 'BT Next Evolution', VER_AZ_THREE_DIGITS) 298 | addAzStyle('NX', 'Net Transport') 299 | addAzStyle('OS', 'OneSwarm', VER_AZ_FOUR_DIGITS) 300 | addAzStyle('OT', 'OmegaTorrent') 301 | addAzStyle('PC', 'CacheLogic', '12.3-4') 302 | addAzStyle('PT', 'Popcorn Time') 303 | addAzStyle('PD', 'Pando') 304 | addAzStyle('PE', 'PeerProject') 305 | addAzStyle('pX', 'pHoeniX') 306 | addAzStyle('qB', 'qBittorrent', VER_AZ_DELUGE) 307 | addAzStyle('QD', 'qqdownload') 308 | addAzStyle('RM', 'RUM Torrent') 309 | addAzStyle('RT', 'Retriever') 310 | addAzStyle('RZ', 'RezTorrent') 311 | addAzStyle('S~', 'Shareaza alpha/beta') 312 | addAzStyle('SB', 'SwiftBit') 313 | addAzStyle('SD', '\u8FC5\u96F7\u5728\u7EBF (Xunlei)')// Apparently, the English name of the client is "Thunderbolt". 314 | addAzStyle('SG', 'GS Torrent', VER_AZ_FOUR_DIGITS) 315 | addAzStyle('SN', 'ShareNET') 316 | addAzStyle('SP', 'BitSpirit', VER_AZ_THREE_DIGITS)// >= 3.6 317 | addAzStyle('SS', 'SwarmScope') 318 | addAzStyle('ST', 'SymTorrent', '2.34') 319 | addAzStyle('st', 'SharkTorrent') 320 | addAzStyle('SZ', 'Shareaza') 321 | addAzStyle('TG', 'Torrent GO') 322 | addAzStyle('TN', 'Torrent.NET') 323 | addAzStyle('TR', 'Transmission', VER_AZ_TRANSMISSION_STYLE) 324 | addAzStyle('TS', 'TorrentStorm') 325 | addAzStyle('TT', 'TuoTu', VER_AZ_THREE_DIGITS) 326 | addAzStyle('UL', 'uLeecher!') 327 | addAzStyle('UE', '\u00B5Torrent Embedded', VER_AZ_THREE_DIGITS_PLUS_MNEMONIC) 328 | addAzStyle('UT', '\u00B5Torrent', VER_AZ_THREE_DIGITS_PLUS_MNEMONIC) 329 | addAzStyle('UM', '\u00B5Torrent Mac', VER_AZ_THREE_DIGITS_PLUS_MNEMONIC) 330 | addAzStyle('UW', '\u00B5Torrent Web', VER_AZ_THREE_DIGITS_PLUS_MNEMONIC) 331 | addAzStyle('WD', 'WebTorrent Desktop', VER_AZ_WEBTORRENT_STYLE)// Go Webtorrent!! :) 332 | addAzStyle('WT', 'Bitlet') 333 | addAzStyle('WW', 'WebTorrent', VER_AZ_WEBTORRENT_STYLE)// Go Webtorrent!! :) 334 | addAzStyle('WY', 'FireTorrent')// formerly Wyzo. 335 | addAzStyle('VG', '\u54c7\u560E (Vagaa)', VER_AZ_FOUR_DIGITS) 336 | addAzStyle('XL', '\u8FC5\u96F7\u5728\u7EBF (Xunlei)')// Apparently, the English name of the client is "Thunderbolt". 337 | addAzStyle('XT', 'XanTorrent') 338 | addAzStyle('XF', 'Xfplay', VER_AZ_TRANSMISSION_STYLE) 339 | addAzStyle('XX', 'XTorrent', '1.2.34') 340 | addAzStyle('XC', 'XTorrent', '1.2.34') 341 | addAzStyle('ZT', 'ZipTorrent') 342 | addAzStyle('7T', 'aTorrent') 343 | addAzStyle('ZO', 'Zona', VER_AZ_FOUR_DIGITS) 344 | addAzStyle('#@', 'Invalid PeerID') 345 | 346 | addShadowStyle('A', 'ABC') 347 | addShadowStyle('O', 'Osprey Permaseed') 348 | addShadowStyle('Q', 'BTQueue') 349 | addShadowStyle('R', 'Tribler') 350 | addShadowStyle('S', 'Shad0w') 351 | addShadowStyle('T', 'BitTornado') 352 | addShadowStyle('U', 'UPnP NAT') 353 | 354 | addMainlineStyle('M', 'Mainline') 355 | addMainlineStyle('Q', 'Queen Bee') 356 | 357 | // Simple clients with no version number. 358 | addSimpleClient('\u00B5Torrent', '1.7.0 RC', '-UT170-')// http://forum.utorrent.com/viewtopic.php?pid=260927#p260927 359 | addSimpleClient('Azureus', '1', 'Azureus') 360 | addSimpleClient('Azureus', '2.0.3.2', 'Azureus', 5) 361 | addSimpleClient('Aria', '2', '-aria2-') 362 | addSimpleClient('BitTorrent Plus!', 'II', 'PRC.P---') 363 | addSimpleClient('BitTorrent Plus!', 'P87.P---') 364 | addSimpleClient('BitTorrent Plus!', 'S587Plus') 365 | addSimpleClient('BitTyrant (Azureus Mod)', 'AZ2500BT') 366 | addSimpleClient('Blizzard Downloader', 'BLZ') 367 | addSimpleClient('BTGetit', 'BG', 10) 368 | addSimpleClient('BTugaXP', 'btuga') 369 | addSimpleClient('BTugaXP', 'BTuga', 5) 370 | addSimpleClient('BTugaXP', 'oernu') 371 | addSimpleClient('Deadman Walking', 'BTDWV-') 372 | addSimpleClient('Deadman', 'Deadman Walking-') 373 | addSimpleClient('External Webseed', 'Ext') 374 | addSimpleClient('G3 Torrent', '-G3') 375 | addSimpleClient('GreedBT', '2.7.1', '271-') 376 | addSimpleClient('Hurricane Electric', 'arclight') 377 | addSimpleClient('HTTP Seed', '-WS') 378 | addSimpleClient('JVtorrent', '10-------') 379 | addSimpleClient('Limewire', 'LIME') 380 | addSimpleClient('Martini Man', 'martini') 381 | addSimpleClient('Pando', 'Pando') 382 | addSimpleClient('PeerApp', 'PEERAPP') 383 | addSimpleClient('SimpleBT', 'btfans', 4) 384 | addSimpleClient('Swarmy', 'a00---0') 385 | addSimpleClient('Swarmy', 'a02---0') 386 | addSimpleClient('Teeweety', 'T00---0') 387 | addSimpleClient('TorrentTopia', '346-') 388 | addSimpleClient('XanTorrent', 'DansClient') 389 | addSimpleClient('MediaGet', '-MG1') 390 | addSimpleClient('MediaGet', '2.1', '-MG21') 391 | 392 | /** 393 | * This is interesting - it uses Mainline style, except uses two characters instead of one. 394 | * And then - the particular numbering style it uses would actually break the way we decode 395 | * version numbers (our code is too hardcoded to "-x-y-z--" style version numbers). 396 | * 397 | * This should really be declared as a Mainline style peer ID, but I would have to 398 | * make my code more generic. Not a bad thing - just something I'm not doing right 399 | * now. 400 | */ 401 | addSimpleClient('Amazon AWS S3', 'S3-') 402 | 403 | // Simple clients with custom version schemes 404 | // TODO: support custom version schemes 405 | addSimpleClient('BitTorrent DNA', 'DNA') 406 | addSimpleClient('Opera', 'OP')// Pre build 10000 versions 407 | addSimpleClient('Opera', 'O')// Post build 10000 versions 408 | addSimpleClient('Burst!', 'Mbrst') 409 | addSimpleClient('TurboBT', 'turbobt') 410 | addSimpleClient('BT Protocol Daemon', 'btpd') 411 | addSimpleClient('Plus!', 'Plus') 412 | addSimpleClient('XBT', 'XBT') 413 | addSimpleClient('BitsOnWheels', '-BOW') 414 | addSimpleClient('eXeem', 'eX') 415 | addSimpleClient('MLdonkey', '-ML') 416 | addSimpleClient('Bitlet', 'BitLet') 417 | addSimpleClient('AllPeers', 'AP') 418 | addSimpleClient('BTuga Revolution', 'BTM') 419 | addSimpleClient('Rufus', 'RS', 2) 420 | addSimpleClient('BitMagnet', 'BM', 2)// BitMagnet - predecessor to Rufus 421 | addSimpleClient('QVOD', 'QVOD') 422 | // Top-BT is based on BitTornado, but doesn't quite stick to Shadow's naming conventions, 423 | // so we'll use substring matching instead. 424 | addSimpleClient('Top-BT', 'TB') 425 | addSimpleClient('Tixati', 'TIX') 426 | // seems to have a sub-version encoded in following 3 bytes, not worked out how: "folx/1.0.456.591" : 2D 464C 3130 FF862D 486263574A43585F66314D5A 427 | addSimpleClient('folx', '-FL') 428 | addSimpleClient('\u00B5Torrent Mac', '-UM') 429 | addSimpleClient('\u00B5Torrent', '-UT') // UT 3.4+ 430 | })() 431 | --------------------------------------------------------------------------------