├── .gitignore ├── .npmignore ├── .npmrc ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── test ├── timeout-destroy.js ├── no-timeout.js ├── timeout.js ├── state-change-on-end.js ├── extension.js └── protocol.js ├── LICENSE ├── package.json ├── README.md ├── CHANGELOG.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !index.js 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 'on': 3 | - push 4 | - pull_request 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 | - '18' 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ runner.node }} 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /test/timeout-destroy.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Protocol from '../index.js' 3 | 4 | test('Timeout and destroy when peer does not respond', t => { 5 | t.plan(4) 6 | 7 | let timeouts = 0 8 | 9 | const wire = new Protocol() 10 | wire.on('error', err => { t.fail(err) }) 11 | wire.pipe(wire) 12 | wire.setTimeout(1000) 13 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 14 | 15 | wire.on('unchoke', () => { 16 | wire.request(0, 0, 0, err => { 17 | t.ok(err) 18 | }) 19 | 20 | wire.request(0, 0, 0, err => { 21 | t.ok(err) 22 | }) 23 | 24 | wire.request(0, 0, 0, err => { 25 | t.ok(err) 26 | }) 27 | }) 28 | 29 | wire.on('timeout', () => { 30 | t.equal(++timeouts, 1) 31 | wire.end() 32 | }) 33 | 34 | wire.unchoke() 35 | }) 36 | -------------------------------------------------------------------------------- /test/no-timeout.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Protocol from '../index.js' 3 | 4 | test('No timeout when peer is good', t => { 5 | t.plan(3) 6 | 7 | const wire = new Protocol() 8 | wire.on('error', err => { t.fail(err) }) 9 | wire.pipe(wire) 10 | wire.setTimeout(1000) 11 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 12 | 13 | wire.on('unchoke', () => { 14 | wire.request(0, 0, 11, err => { 15 | t.error(err) 16 | }) 17 | 18 | wire.request(0, 0, 11, err => { 19 | t.error(err) 20 | }) 21 | 22 | wire.request(0, 0, 11, err => { 23 | t.error(err) 24 | }) 25 | }) 26 | 27 | wire.on('request', (i, offset, length, callback) => { 28 | callback(null, Buffer.from('hello world')) 29 | }) 30 | 31 | // there should never be a timeout 32 | wire.on('timeout', () => { 33 | t.fail('Timed out') 34 | }) 35 | 36 | wire.unchoke() 37 | }) 38 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Protocol from '../index.js' 3 | 4 | test('Timeout when peer does not respond', t => { 5 | t.plan(9) 6 | 7 | let timeouts = 0 8 | 9 | const wire = new Protocol() 10 | wire.on('error', err => { t.fail(err) }) 11 | wire.pipe(wire) 12 | wire.setTimeout(1000) 13 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 14 | 15 | wire.on('unchoke', () => { 16 | let requests = 0 17 | 18 | wire.request(0, 0, 0, err => { 19 | t.ok(err) 20 | t.ok(++requests === 1) 21 | }) 22 | 23 | wire.request(0, 0, 0, err => { 24 | t.ok(err) 25 | t.ok(++requests === 2) 26 | }) 27 | 28 | wire.request(0, 0, 0, err => { 29 | t.ok(err) 30 | t.ok(++requests === 3) 31 | }) 32 | }) 33 | 34 | wire.on('timeout', () => { 35 | t.ok(++timeouts <= 3) // should get called 3 times 36 | }) 37 | 38 | wire.unchoke() 39 | }) 40 | -------------------------------------------------------------------------------- /.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@v3 15 | with: 16 | persist-credentials: false 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | - name: Cache 22 | uses: actions/cache@v3 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 | run: npm i 30 | env: 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 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) Feross Aboukhadijeh, Mathias Buus, 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 | -------------------------------------------------------------------------------- /test/state-change-on-end.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Protocol from '../index.js' 3 | 4 | test('State changes correctly on wire \'end\'', t => { 5 | t.plan(11) 6 | 7 | const wire = new Protocol() 8 | wire.on('error', err => { t.fail(err) }) 9 | wire.pipe(wire) 10 | 11 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 12 | 13 | t.ok(wire.amChoking) 14 | t.ok(wire.peerChoking) 15 | 16 | wire.on('unchoke', () => { 17 | t.ok(!wire.amChoking) 18 | t.ok(!wire.peerChoking) 19 | wire.interested() 20 | }) 21 | 22 | wire.on('interested', () => { 23 | t.ok(wire.peerInterested) 24 | destroy() 25 | }) 26 | 27 | function destroy () { 28 | wire.on('choke', () => { 29 | t.pass('wire got choke event') 30 | }) 31 | wire.on('uninterested', () => { 32 | t.pass('wire got uninterested event') 33 | }) 34 | 35 | wire.on('end', () => { 36 | t.ok(wire.peerChoking) 37 | t.ok(!wire.peerInterested) 38 | }) 39 | 40 | wire.on('finish', () => { 41 | t.ok(wire.peerChoking) 42 | t.ok(!wire.peerInterested) 43 | }) 44 | 45 | wire.destroy() 46 | } 47 | 48 | wire.unchoke() 49 | }) 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bittorrent-protocol", 3 | "type": "module", 4 | "description": "Simple, robust, BitTorrent peer wire protocol implementation", 5 | "version": "4.1.21", 6 | "author": { 7 | "name": "WebTorrent LLC", 8 | "email": "feross@webtorrent.io", 9 | "url": "https://webtorrent.io" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/webtorrent/bittorrent-protocol/issues" 13 | }, 14 | "dependencies": { 15 | "bencode": "^4.0.0", 16 | "bitfield": "^4.2.0", 17 | "debug": "^4.4.3", 18 | "rc4": "^0.1.5", 19 | "streamx": "^2.22.1", 20 | "throughput": "^1.0.2", 21 | "uint8-util": "^2.2.5", 22 | "unordered-array-remove": "^1.0.2" 23 | }, 24 | "devDependencies": { 25 | "@webtorrent/semantic-release-config": "1.0.10", 26 | "semantic-release": "21.1.2", 27 | "standard": "*", 28 | "tap-spec": "^5.0.0", 29 | "tape": "5.9.0" 30 | }, 31 | "engines": { 32 | "node": ">=12.20.0" 33 | }, 34 | "exports": { 35 | "import": "./index.js" 36 | }, 37 | "keywords": [ 38 | "bittorrent", 39 | "p2p", 40 | "peer", 41 | "peer-to-peer", 42 | "protocol", 43 | "stream", 44 | "torrent", 45 | "wire" 46 | ], 47 | "license": "MIT", 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/webtorrent/bittorrent-protocol.git" 51 | }, 52 | "scripts": { 53 | "test": "standard && tape test/*.js | tap-spec" 54 | }, 55 | "funding": [ 56 | { 57 | "type": "github", 58 | "url": "https://github.com/sponsors/feross" 59 | }, 60 | { 61 | "type": "patreon", 62 | "url": "https://www.patreon.com/feross" 63 | }, 64 | { 65 | "type": "consulting", 66 | "url": "https://feross.org/support" 67 | } 68 | ], 69 | "renovate": { 70 | "extends": [ 71 | "github>webtorrent/renovate-config" 72 | ], 73 | "rangeStrategy": "bump" 74 | }, 75 | "release": { 76 | "extends": "@webtorrent/semantic-release-config" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/extension.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Protocol from '../index.js' 3 | 4 | test('Extension.prototype.name', t => { 5 | t.plan(2) 6 | 7 | const wire = new Protocol() 8 | 9 | function NoNameExtension () {} 10 | t.throws(() => { 11 | wire.use(NoNameExtension) 12 | }, 'throws when Extension.prototype.name is undefined') 13 | 14 | function NamedExtension () {} 15 | NamedExtension.prototype.name = 'named_extension' 16 | t.doesNotThrow(() => { 17 | wire.use(NamedExtension) 18 | }, 'does not throw when Extension.prototype.name is defined') 19 | }) 20 | 21 | test('Extension.onHandshake', t => { 22 | t.plan(4) 23 | 24 | function TestExtension () {} 25 | TestExtension.prototype.name = 'test_extension' 26 | TestExtension.prototype.onHandshake = (infoHash, peerId, extensions) => { 27 | t.equal(Buffer.from(infoHash, 'hex').length, 20) 28 | t.equal(Buffer.from(infoHash, 'hex').toString(), '01234567890123456789') 29 | t.equal(Buffer.from(peerId, 'hex').length, 20) 30 | t.equal(Buffer.from(peerId, 'hex').toString(), '12345678901234567890') 31 | } 32 | 33 | const wire = new Protocol() 34 | wire.on('error', err => { t.fail(err) }) 35 | wire.pipe(wire) 36 | 37 | wire.use(TestExtension) 38 | 39 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 40 | }) 41 | 42 | test('Extension.onExtendedHandshake', t => { 43 | t.plan(3) 44 | 45 | class TestExtension { 46 | constructor (wire) { 47 | wire.extendedHandshake = { 48 | hello: 'world!' 49 | } 50 | } 51 | 52 | onExtendedHandshake (handshake) { 53 | t.ok(handshake.m.test_extension, 'peer extended handshake includes extension name') 54 | t.equal(Buffer.from(handshake.hello).toString(), 'world!', 'peer extended handshake includes extension-defined parameters') 55 | } 56 | } 57 | 58 | TestExtension.prototype.name = 'test_extension' 59 | 60 | const wire = new Protocol() // incoming 61 | wire.on('error', err => { t.fail(err) }) 62 | wire.pipe(wire) 63 | 64 | wire.once('handshake', (infoHash, peerId, extensions) => { 65 | t.equal(extensions.extended, true) 66 | }) 67 | 68 | wire.use(TestExtension) 69 | 70 | wire.handshake('3031323334353637383930313233343536373839', '3132333435363738393031323334353637383930') 71 | }) 72 | 73 | test('Extension.onMessage', t => { 74 | t.plan(1) 75 | 76 | class TestExtension { 77 | constructor (wire) { 78 | this.wire = wire 79 | } 80 | 81 | onMessage (message) { 82 | t.equal(Buffer.from(message).toString(), 'hello world!', 'receives message sent with wire.extended()') 83 | } 84 | } 85 | 86 | TestExtension.prototype.name = 'test_extension' 87 | 88 | const wire = new Protocol() // outgoing 89 | wire.on('error', err => { t.fail(err) }) 90 | wire.pipe(wire) 91 | 92 | wire.use(TestExtension) 93 | 94 | wire.handshake('3031323334353637383930313233343536373839', '3132333435363738393031323334353637383930') 95 | 96 | wire.once('extended', () => { 97 | wire.extended('test_extension', Buffer.from('hello world!')) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bittorrent-protocol [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/webtorrent/bittorrent-protocol/master.svg 4 | [travis-url]: https://travis-ci.org/webtorrent/bittorrent-protocol 5 | [npm-image]: https://img.shields.io/npm/v/bittorrent-protocol.svg 6 | [npm-url]: https://npmjs.org/package/bittorrent-protocol 7 | [downloads-image]: https://img.shields.io/npm/dm/bittorrent-protocol.svg 8 | [downloads-url]: https://npmjs.org/package/bittorrent-protocol 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | ### Simple, robust, BitTorrent wire protocol implementation 13 | 14 | Node.js implementation of the [BitTorrent peer wire protocol](https://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29). 15 | The protocol is the main communication layer for BitTorrent file transfer. 16 | 17 | Also works in the browser with [browserify](http://browserify.org/)! This module is used 18 | by [WebTorrent](http://webtorrent.io). 19 | 20 | ## install 21 | 22 | ``` 23 | npm install bittorrent-protocol 24 | ``` 25 | 26 | ## usage 27 | 28 | The protocol is implemented as a **duplex stream**, so all you have to do is pipe to and 29 | from it. 30 | 31 | duplex streams | a.pipe(b).pipe(a) 32 | ---- | --- 33 | ![duplex streams](https://raw.github.com/substack/lxjs-stream-examples/master/images/duplex_streams.png) | ![a.pipe(b).pipe(a)](https://raw.github.com/substack/lxjs-stream-examples/master/images/a_pipe_b_pipe_a.png) 34 | 35 | (Images from the ["harnessing streams"](https://github.com/substack/lxjs-stream-examples/blob/master/slides.markdown) talk by substack.) 36 | 37 | ```js 38 | import Protocol from 'bittorrent-protocol' 39 | import net from 'net' 40 | 41 | net.createServer(socket => { 42 | const wire = new Protocol() 43 | 44 | // pipe to and from the protocol 45 | socket.pipe(wire).pipe(socket) 46 | 47 | wire.on('handshake', (infoHash, peerId) => { 48 | // receive a handshake (infoHash and peerId are hex strings) 49 | 50 | // lets emit a handshake of our own as well 51 | wire.handshake('my info hash (hex)', 'my peer id (hex)') 52 | }) 53 | 54 | wire.on('unchoke', () => { 55 | console.log('peer is no longer choking us: ' + wire.peerChoking) 56 | }) 57 | }).listen(6881) 58 | ``` 59 | 60 | ## methods 61 | 62 | ### handshaking 63 | 64 | Send and receive a handshake from the peer. This is the first message. 65 | 66 | ```js 67 | // send a handshake to the peer 68 | wire.handshake(infoHash, peerId, { dht: true }) 69 | wire.on('handshake', (infoHash, peerId, extensions) => { 70 | // receive a handshake (infoHash and peerId are hex strings) 71 | console.log(extensions.dht) // supports DHT (BEP-0005) 72 | console.log(extensions.extended) // supports extension protocol (BEP-0010) 73 | }) 74 | ``` 75 | 76 | For `wire.handshake()`, the `infoHash` and the `peerId` should be 20 bytes (hex-encoded `string` or `Buffer`). 77 | 78 | ### choking 79 | 80 | Check if you or the peer is choking. 81 | 82 | ```js 83 | wire.peerChoking // is the peer choking us? 84 | wire.amChoking // are we choking the peer? 85 | 86 | wire.on('choke', () => { 87 | // the peer is now choking us 88 | }) 89 | wire.on('unchoke', () => { 90 | // peer is no longer choking us 91 | }) 92 | ``` 93 | 94 | ### interested 95 | 96 | See if you or the peer is interested. 97 | 98 | ```js 99 | wire.peerInterested // is the peer interested in us? 100 | wire.amInterested // are we interested in the peer? 101 | 102 | wire.on('interested', () => { 103 | // peer is now interested 104 | }) 105 | wire.on('uninterested', () => { 106 | // peer is no longer interested 107 | }) 108 | ``` 109 | 110 | ### bitfield 111 | 112 | Exchange piece information with the peer. 113 | 114 | ```js 115 | // send a bitfield to the peer 116 | wire.bitfield(buffer) 117 | wire.on('bitfield', bitfield => { 118 | // bitfield received from the peer 119 | }) 120 | 121 | // send a have message indicating that you have a piece 122 | wire.have(pieceIndex) 123 | wire.on('have', pieceIndex => { 124 | // peer has sent you a have message 125 | }) 126 | ``` 127 | 128 | You can always see which pieces the peer has 129 | 130 | ```js 131 | wire.peerPieces.get(i) // returns true if peer has piece i 132 | ``` 133 | 134 | `wire.peerPieces` is a `BitField`, see [docs](https://www.npmjs.org/package/bitfield). 135 | 136 | ### requests 137 | 138 | Send and respond to requests for pieces. 139 | 140 | ```js 141 | // request a block from a peer 142 | wire.request(pieceIndex, offset, length, (err, block) => { 143 | if (err) { 144 | // there was an error (peer has started choking us etc) 145 | return 146 | } 147 | // got block 148 | }) 149 | 150 | // cancel a request to a peer 151 | wire.cancel(pieceIndex, offset, length) 152 | 153 | // receive a request from a peer 154 | wire.on('request', (pieceIndex, offset, length, callback) => { 155 | // ... read block ... 156 | callback(null, block) // respond back to the peer 157 | }) 158 | 159 | wire.requests // list of requests we currently have pending {piece, offset, length} 160 | wire.peerRequests // list of requests the peer currently have pending {piece, offset, length} 161 | ``` 162 | 163 | You can set a request timeout if you want to. 164 | 165 | ```js 166 | wire.setTimeout(5000) // head request should take a most 5s to finish 167 | ``` 168 | 169 | If the timeout is triggered the request callback is called with an error and a `timeout` 170 | event is emitted. 171 | 172 | ### dht and port 173 | 174 | You can set the extensions flag `dht` in the handshake to `true` if you participate in 175 | the torrent dht. Afterwards you can send your dht port. 176 | 177 | ```js 178 | // send your port to the peer 179 | wire.port(dhtPort) 180 | wire.on('port', dhtPort => { 181 | // peer has sent a port to us 182 | }) 183 | ``` 184 | 185 | You can check to see if the peer supports extensions. 186 | 187 | ```js 188 | wire.peerExtensions.dht // supports DHT (bep_0005) 189 | wire.peerExtensions.extended // supports extended messages (bep_0005) 190 | ``` 191 | 192 | ### keep-alive 193 | 194 | You can enable the keep-alive ping (triggered every 60s). 195 | 196 | ```js 197 | // starts the keep alive 198 | wire.setKeepAlive(true) 199 | wire.on('keep-alive', () => { 200 | // peer sent a keep alive - just ignore it 201 | }) 202 | ``` 203 | ### fast extension (BEP 6) 204 | 205 | This module has built-in support for the 206 | [BitTorrent Fast Extension (BEP 6)](http://www.bittorrent.org/beps/bep_0006.html). 207 | 208 | The Fast Extension introduces several messages to make the protocol more efficient: 209 | have-none, have-all, suggest, reject, and allowed-fast. 210 | 211 | ```js 212 | wire.handshake(infoHash, peerId, { fast: true }) 213 | 214 | wire.hasFast // true if Fast Extension is available, required to call the following methods 215 | 216 | wire.haveNone() // instead of wire.bitfield(buffer) with an all-zero buffer 217 | wire.on('have-none', () => { 218 | // instead of bitfield with an all-zero buffer 219 | }) 220 | 221 | wire.haveAll() // instead of wire.bitfield(buffer) with an all-one buffer 222 | wire.on('have-all', () => { 223 | // instead of bitfield with an all-one buffer 224 | }) 225 | 226 | wire.suggest(pieceIndex) // suggest requesting a piece to the peer 227 | wire.on('suggest', (pieceIndex) => { 228 | // peer suggests requesting piece 229 | }) 230 | 231 | wire.on('allowed-fast', (pieceIndex) => { 232 | // piece may be obtained from peer while choked 233 | }) 234 | 235 | wire.peerAllowedFastSet // list of allowed-fast pieces 236 | 237 | // Note rejection is handled automatically on choke or request error 238 | wire.reject(pieceIndex, offset, length) // reject a request 239 | wire.on('reject', (pieceIndex, offset, length) => { 240 | // peer rejected a request 241 | }) 242 | ``` 243 | 244 | ### extension protocol (BEP 10) 245 | 246 | This module has built-in support for the 247 | [BitTorrent Extension Protocol (BEP 10)](http://www.bittorrent.org/beps/bep_0010.html). 248 | 249 | The intention of BEP 10 is to provide a simple and thin transport for extensions to the 250 | bittorrent protocol. Most extensions to the protocol use BEP 10 so they can add new 251 | features to the protocol without interfering with the standard bittorrent protocol or 252 | clients that don't support the new extension. 253 | 254 | An example of a BitTorrent extension that uses BEP 10 is 255 | [ut_metadata](http://www.bittorrent.org/beps/bep_0009.html) (BEP 9), the extension that 256 | allows magnet uris to work. 257 | 258 | ```js 259 | wire.extended(code, buffer) 260 | ``` 261 | 262 | This package, **bittorrent-protocol**, also provides an extension API to make it easy to 263 | add extensions to this module using the "extension protocol" (BEP 10). For example, to 264 | support ut_metadata (BEP 9), you need only install the 265 | [ut_metadata](https://www.npmjs.com/package/ut_metadata) npm module and call `wire.use()`. 266 | See the [Extension API](#extension-api) section for more information. 267 | 268 | ### transfer stats 269 | 270 | Check how many bytes you have uploaded and download, and current speed 271 | 272 | ```js 273 | wire.uploaded // number of bytes uploaded 274 | wire.downloaded // number of bytes downloaded 275 | 276 | wire.uploadSpeed() // upload speed - bytes per second 277 | wire.downloadSpeed() // download speed - bytes per second 278 | 279 | wire.on('download', numberOfBytes => { 280 | ... 281 | }) 282 | wire.on('upload', numberOfBytes => { 283 | ... 284 | }) 285 | ``` 286 | 287 | 288 | ## extension api 289 | 290 | This package supports a simple extension API so you can extend the default protocol 291 | functionality with common protocol extensions like ut_metadata (magnet uris). 292 | 293 | Here are the **bittorrent-protocol** extensions that we know about: 294 | 295 | - [ut_metadata](https://www.npmjs.com/package/ut_metadata) - Extension for Peers to Send Metadata Files (BEP 9) 296 | - [ut_pex](https://www.npmjs.com/package/ut_pex) - Extension for Peer Discovery (PEX) 297 | - *Add yours here! Send a pull request!* 298 | 299 | In short, an extension can register itself with at a certain name, which will be added to 300 | the extended protocol handshake sent to the remote peer. Extensions can also hook events 301 | like 'handshake' and 'extended'. To use an extension, simply require it and call 302 | `wire.use()`. 303 | 304 | Here is an example of the **ut_metadata** extension being used with 305 | **bittorrent-protocol**: 306 | 307 | ```js 308 | import Protocol from 'bittorrent-protocol' 309 | import net from 'net' 310 | import ut_metadata from 'ut_metadata' 311 | 312 | net.createServer(socket => { 313 | const wire = new Protocol() 314 | socket.pipe(wire).pipe(socket) 315 | 316 | // initialize the extension 317 | wire.use(ut_metadata()) 318 | 319 | // all `ut_metadata` functionality can now be accessed at wire.ut_metadata 320 | 321 | // ask the peer to send us metadata 322 | wire.ut_metadata.fetch() 323 | 324 | // 'metadata' event will fire when the metadata arrives and is verified to be correct! 325 | wire.ut_metadata.on('metadata', metadata => { 326 | // got metadata! 327 | 328 | // Note: the event will not fire if the peer does not support ut_metadata, if they 329 | // don't have metadata yet either, if they repeatedly send invalid data, or if they 330 | // simply don't respond. 331 | }) 332 | 333 | // optionally, listen to the 'warning' event if you want to know that metadata is 334 | // probably not going to arrive for one of the above reasons. 335 | wire.ut_metadata.on('warning', err => { 336 | console.log(err.message) 337 | }) 338 | 339 | // handle handshake 340 | wire.on('handshake', (infoHash, peerId) => { 341 | // receive a handshake (infoHash and peerId are hex strings) 342 | wire.handshake(new Buffer('my info hash'), new Buffer('my peer id')) 343 | }) 344 | 345 | }).listen(6881) 346 | ``` 347 | 348 | If you want to write your own extension, take a look at the 349 | [ut_metadata index.js file](https://github.com/webtorrent/ut_metadata/blob/master/index.js) 350 | to see how it's done. 351 | 352 | ## license 353 | 354 | MIT. Copyright (c) [Feross Aboukhadijeh](https://feross.org), Mathias Buus, and [WebTorrent, LLC](https://webtorrent.io). 355 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.1.21](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.20...v4.1.21) (2025-09-14) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependency debug to ^4.4.3 ([#163](https://github.com/webtorrent/bittorrent-protocol/issues/163)) ([be2f398](https://github.com/webtorrent/bittorrent-protocol/commit/be2f39875d43bd39476ca69834c8f49e9b749cb1)) 7 | 8 | ## [4.1.20](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.19...v4.1.20) (2025-08-05) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency throughput to ^1.0.2 ([#157](https://github.com/webtorrent/bittorrent-protocol/issues/157)) ([f4a89cd](https://github.com/webtorrent/bittorrent-protocol/commit/f4a89cd1b4113f61c25ae15a91f9b52e17b9c633)) 14 | 15 | ## [4.1.19](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.18...v4.1.19) (2025-08-03) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **perf:** dont use dataview, dont use slice ([#156](https://github.com/webtorrent/bittorrent-protocol/issues/156)) ([caa19f2](https://github.com/webtorrent/bittorrent-protocol/commit/caa19f20a3de3f7e72641fba40c6e660afc2eedc)) 21 | 22 | ## [4.1.18](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.17...v4.1.18) (2025-06-28) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency streamx to ^2.22.1 ([#140](https://github.com/webtorrent/bittorrent-protocol/issues/140)) ([8d8e233](https://github.com/webtorrent/bittorrent-protocol/commit/8d8e23332ba627844c691c3934f11a9cb91c0637)) 28 | 29 | ## [4.1.17](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.16...v4.1.17) (2025-06-28) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** update dependency bitfield to ^4.2.0 ([#141](https://github.com/webtorrent/bittorrent-protocol/issues/141)) ([e0c4051](https://github.com/webtorrent/bittorrent-protocol/commit/e0c4051d4eca7b606d136d9ed1a177e04f3dd428)) 35 | * **deps:** update dependency debug to ^4.4.1 ([#154](https://github.com/webtorrent/bittorrent-protocol/issues/154)) ([230c240](https://github.com/webtorrent/bittorrent-protocol/commit/230c2401823847fca26d89fe2394039d552a2c8b)) 36 | * PE/MSE encryption ([#155](https://github.com/webtorrent/bittorrent-protocol/issues/155)) ([4c252dc](https://github.com/webtorrent/bittorrent-protocol/commit/4c252dc39ef8355e478f32b59b51d999cfb1e2b9)) 37 | 38 | ## [4.1.16](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.15...v4.1.16) (2024-12-07) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **deps:** update dependency debug to ^4.4.0 ([#153](https://github.com/webtorrent/bittorrent-protocol/issues/153)) ([efd2b2a](https://github.com/webtorrent/bittorrent-protocol/commit/efd2b2ab5abeaf694f4ffee763eb5be8c1cd8183)) 44 | 45 | ## [4.1.15](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.14...v4.1.15) (2024-09-07) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * **deps:** update dependency debug to ^4.3.7 ([f89b4f0](https://github.com/webtorrent/bittorrent-protocol/commit/f89b4f0daca13fe1e1d714a17a0c783902c6974d)) 51 | 52 | ## [4.1.14](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.13...v4.1.14) (2024-07-28) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **deps:** update dependency debug to ^4.3.6 ([2649fd8](https://github.com/webtorrent/bittorrent-protocol/commit/2649fd86024f59faed8d6b15b68e13adb2f3028b)) 58 | 59 | ## [4.1.13](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.12...v4.1.13) (2024-06-28) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * **deps:** update dependency uint8-util to ^2.2.5 ([#130](https://github.com/webtorrent/bittorrent-protocol/issues/130)) ([f92477c](https://github.com/webtorrent/bittorrent-protocol/commit/f92477c94dbaa3566e15d7eca7728f5062017031)) 65 | 66 | ## [4.1.12](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.11...v4.1.12) (2024-06-01) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **deps:** update dependency debug to ^4.3.5 ([38c6c8a](https://github.com/webtorrent/bittorrent-protocol/commit/38c6c8a7b7c1a37a38f57e7f6a0915b58cdb204b)) 72 | 73 | ## [4.1.11](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.10...v4.1.11) (2023-08-11) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * **deps:** update dependency streamx to ^2.15.1 ([#126](https://github.com/webtorrent/bittorrent-protocol/issues/126)) ([b138d12](https://github.com/webtorrent/bittorrent-protocol/commit/b138d1248f861ca88e812201e261a2ba744f98af)) 79 | * **deps:** update dependency uint8-util to ^2.2.2 ([#124](https://github.com/webtorrent/bittorrent-protocol/issues/124)) ([3ff2481](https://github.com/webtorrent/bittorrent-protocol/commit/3ff24811f9063da9a47cccbd06c3e013804025a9)) 80 | 81 | ## [4.1.10](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.9...v4.1.10) (2023-08-10) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **deps:** update dependency bencode to v4 ([#127](https://github.com/webtorrent/bittorrent-protocol/issues/127)) ([66aecc3](https://github.com/webtorrent/bittorrent-protocol/commit/66aecc36daff3783ad0ce2c3faed8cfe79a58482)) 87 | 88 | ## [4.1.9](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.8...v4.1.9) (2023-07-31) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * **deps:** update dependency bencode to ^3.1.1 ([d53dc2f](https://github.com/webtorrent/bittorrent-protocol/commit/d53dc2fc335d47838aa9e432667f7c0b0f53d62d)) 94 | 95 | ## [4.1.8](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.7...v4.1.8) (2023-07-23) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * **deps:** update dependency streamx to ^2.15.0 ([#118](https://github.com/webtorrent/bittorrent-protocol/issues/118)) ([e23925c](https://github.com/webtorrent/bittorrent-protocol/commit/e23925c9b615724c3db816aee6effb626ea01bf6)) 101 | 102 | ## [4.1.7](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.6...v4.1.7) (2023-05-30) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * **deps:** update dependency streamx to ^2.13.2 ([#117](https://github.com/webtorrent/bittorrent-protocol/issues/117)) ([c09a7f6](https://github.com/webtorrent/bittorrent-protocol/commit/c09a7f6b6a61cc979f09934712eafdb4f4019edd)) 108 | * **deps:** update dependency uint8-util to ^2.1.9 ([#110](https://github.com/webtorrent/bittorrent-protocol/issues/110)) ([9d3733c](https://github.com/webtorrent/bittorrent-protocol/commit/9d3733c329f3d3bbf3e957335bae0c24ae88b49f)) 109 | * migrate to streamx ([#96](https://github.com/webtorrent/bittorrent-protocol/issues/96)) ([9b77f6c](https://github.com/webtorrent/bittorrent-protocol/commit/9b77f6c1f5c27092e4e656fc448e79f295b9cb30)) 110 | 111 | ## [4.1.6](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.5...v4.1.6) (2023-01-31) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * **deps:** update dependency uint8-util to ^2.1.7 ([#106](https://github.com/webtorrent/bittorrent-protocol/issues/106)) ([78b0f4a](https://github.com/webtorrent/bittorrent-protocol/commit/78b0f4af8753bba4bb46cf812c96f2c2bd013365)) 117 | 118 | ## [4.1.5](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.4...v4.1.5) (2023-01-31) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * **deps:** update dependency bencode to ^3.0.3 ([#108](https://github.com/webtorrent/bittorrent-protocol/issues/108)) ([580a15c](https://github.com/webtorrent/bittorrent-protocol/commit/580a15c1a53f9f7b3e8f9a04e3e176072d549323)), closes [#109](https://github.com/webtorrent/bittorrent-protocol/issues/109) 124 | 125 | ## [4.1.4](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.3...v4.1.4) (2023-01-31) 126 | 127 | 128 | ### Bug Fixes 129 | 130 | * **deps:** update dependency bencode to ^3.0.2 ([f1582e5](https://github.com/webtorrent/bittorrent-protocol/commit/f1582e56c342e4a75c9ba134b3ecd53affaab77b)) 131 | 132 | ## [4.1.3](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.2...v4.1.3) (2023-01-26) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * **deps:** update dependency uint8-util to ^2.1.5 ([#105](https://github.com/webtorrent/bittorrent-protocol/issues/105)) ([d16dbdd](https://github.com/webtorrent/bittorrent-protocol/commit/d16dbdd5536e61e5d2cd045286e44fbcdd9064e2)) 138 | 139 | ## [4.1.2](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.1...v4.1.2) (2023-01-25) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * **deps:** update dependency uint8-util to ^2.1.4 ([#100](https://github.com/webtorrent/bittorrent-protocol/issues/100)) ([b77b355](https://github.com/webtorrent/bittorrent-protocol/commit/b77b3555a4dc98dc41025d35420a0f0661cf574c)) 145 | 146 | ## [4.1.1](https://github.com/webtorrent/bittorrent-protocol/compare/v4.1.0...v4.1.1) (2023-01-25) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * **deps:** update dependency bencode to v3 ([#101](https://github.com/webtorrent/bittorrent-protocol/issues/101)) ([32e02d1](https://github.com/webtorrent/bittorrent-protocol/commit/32e02d14b533c77891b429a1d52135ade47dada8)) 152 | 153 | # [4.1.0](https://github.com/webtorrent/bittorrent-protocol/compare/v4.0.1...v4.1.0) (2022-12-15) 154 | 155 | 156 | ### Features 157 | 158 | * use uint8 instead of buffer ([#99](https://github.com/webtorrent/bittorrent-protocol/issues/99)) ([ad7de65](https://github.com/webtorrent/bittorrent-protocol/commit/ad7de65366fb5c89813a18356422f365bec0da50)) 159 | 160 | ## [4.0.1](https://github.com/webtorrent/bittorrent-protocol/compare/v4.0.0...v4.0.1) (2022-05-14) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * replace speedometer with throughput ([#92](https://github.com/webtorrent/bittorrent-protocol/issues/92)) ([642ac8e](https://github.com/webtorrent/bittorrent-protocol/commit/642ac8e5e2823a7bf3be740246f9f15cf13f17d2)) 166 | 167 | # [4.0.0](https://github.com/webtorrent/bittorrent-protocol/compare/v3.5.4...v4.0.0) (2022-04-28) 168 | 169 | 170 | ### chore 171 | 172 | * release 4 ([08f56ec](https://github.com/webtorrent/bittorrent-protocol/commit/08f56ec8323a4a51922192b98da2c76bb041f0c8)) 173 | 174 | 175 | ### BREAKING CHANGES 176 | 177 | * ESM only 178 | 179 | ## [3.5.4](https://github.com/webtorrent/bittorrent-protocol/compare/v3.5.3...v3.5.4) (2022-04-28) 180 | 181 | 182 | ### Code Refactoring 183 | 184 | * switch to ESM ([#90](https://github.com/webtorrent/bittorrent-protocol/issues/90)) ([fce2548](https://github.com/webtorrent/bittorrent-protocol/commit/fce254818590b307afb45a3fdaa8e4dc904305ce)) 185 | 186 | 187 | ### BREAKING CHANGES 188 | 189 | * ESM only 190 | 191 | * chore: update imports and export index.js 192 | 193 | esm import/export syntax 194 | Signed-off-by: Lakshya Singh 195 | 196 | * chore: update imports in tests 197 | 198 | esm import syntax with path 199 | Signed-off-by: Lakshya Singh 200 | 201 | * chore: bump bitfield for esm 202 | 203 | 4.1.0 is esm based while 4.0.0 was commonjs 204 | Signed-off-by: Lakshya Singh 205 | 206 | * chore: update package.json for esm 207 | 208 | specify minimum nodejs version for esm support 209 | exports defined 210 | type change to module 211 | Signed-off-by: Lakshya Singh 212 | 213 | * chore: update readme with esm syntax 214 | 215 | Signed-off-by: Lakshya Singh 216 | 217 | ## [3.5.3](https://github.com/webtorrent/bittorrent-protocol/compare/v3.5.2...v3.5.3) (2022-04-22) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * infinite loop when an allowed-fast request is pending on choke ([#88](https://github.com/webtorrent/bittorrent-protocol/issues/88)) ([a3d28da](https://github.com/webtorrent/bittorrent-protocol/commit/a3d28dac8bcf05af5dd12fe82dfbc7abeed4c55a)) 223 | 224 | ## [3.5.2](https://github.com/webtorrent/bittorrent-protocol/compare/v3.5.1...v3.5.2) (2022-03-27) 225 | 226 | 227 | ### Bug Fixes 228 | 229 | * **deps:** update dependency debug to ^4.3.4 ([#85](https://github.com/webtorrent/bittorrent-protocol/issues/85)) ([117ecf3](https://github.com/webtorrent/bittorrent-protocol/commit/117ecf325714142f7643d8cedf434bc58faabb96)) 230 | 231 | ## [3.5.1](https://github.com/webtorrent/bittorrent-protocol/compare/v3.5.0...v3.5.1) (2022-01-20) 232 | 233 | 234 | ### Bug Fixes 235 | 236 | * reject on error and activation guards for Fast Extension ([#79](https://github.com/webtorrent/bittorrent-protocol/issues/79)) ([d59075b](https://github.com/webtorrent/bittorrent-protocol/commit/d59075bbb13a3c1ef6baaa64601bf8d2f950bbc2)) 237 | 238 | # [3.5.0](https://github.com/webtorrent/bittorrent-protocol/compare/v3.4.5...v3.5.0) (2022-01-17) 239 | 240 | 241 | ### Features 242 | 243 | * add BEP6 Fast Extension messages ([#75](https://github.com/webtorrent/bittorrent-protocol/issues/75)) ([319136d](https://github.com/webtorrent/bittorrent-protocol/commit/319136d7146135abfb25deade4ae5693d309e79f)) 244 | 245 | ## [3.4.5](https://github.com/webtorrent/bittorrent-protocol/compare/v3.4.4...v3.4.5) (2022-01-17) 246 | 247 | 248 | ### Bug Fixes 249 | 250 | * return `this` from `destroy` and `end` ([#74](https://github.com/webtorrent/bittorrent-protocol/issues/74)) ([cba86e5](https://github.com/webtorrent/bittorrent-protocol/commit/cba86e5aff9492b45279cd6ded77e1af3db2c6b5)) 251 | 252 | ## [3.4.4](https://github.com/webtorrent/bittorrent-protocol/compare/v3.4.3...v3.4.4) (2022-01-17) 253 | 254 | 255 | ### Bug Fixes 256 | 257 | * **deps:** update dependency bencode to ^2.0.2 ([#63](https://github.com/webtorrent/bittorrent-protocol/issues/63)) ([c022e17](https://github.com/webtorrent/bittorrent-protocol/commit/c022e17efe9d28aaf0c25a087abe75fe27549742)) 258 | * **deps:** update dependency debug to ^4.3.3 ([#64](https://github.com/webtorrent/bittorrent-protocol/issues/64)) ([2f2d84c](https://github.com/webtorrent/bittorrent-protocol/commit/2f2d84c7d88b296c98b784da9dca570045630d55)) 259 | 260 | ## [3.4.3](https://github.com/webtorrent/bittorrent-protocol/compare/v3.4.2...v3.4.3) (2021-08-04) 261 | 262 | 263 | ### Bug Fixes 264 | 265 | * **deps:** update dependency simple-sha1 to ^3.1.0 ([f3083f6](https://github.com/webtorrent/bittorrent-protocol/commit/f3083f687bf15d351654b2b4a44b3eab6b47188c)) 266 | 267 | ## [3.4.2](https://github.com/webtorrent/bittorrent-protocol/compare/v3.4.1...v3.4.2) (2021-07-08) 268 | 269 | ## [3.4.1](https://github.com/webtorrent/bittorrent-protocol/compare/v3.4.0...v3.4.1) (2021-06-15) 270 | 271 | 272 | ### Bug Fixes 273 | 274 | * modernize ([3d3e244](https://github.com/webtorrent/bittorrent-protocol/commit/3d3e244319036583230d64824ce1388287233e02)) 275 | 276 | # [3.4.0](https://github.com/webtorrent/bittorrent-protocol/compare/v3.3.2...v3.4.0) (2021-06-15) 277 | 278 | 279 | ### Features 280 | 281 | * PE/MSE Implementation for WebTorrent - update for new version ([#48](https://github.com/webtorrent/bittorrent-protocol/issues/48)) ([14f9d81](https://github.com/webtorrent/bittorrent-protocol/commit/14f9d81d07a0d49e4b9460c5392b88bdf0f7bf00)) 282 | 283 | ## [3.3.2](https://github.com/webtorrent/bittorrent-protocol/compare/v3.3.1...v3.3.2) (2021-06-15) 284 | 285 | 286 | ### Bug Fixes 287 | 288 | * package.json ([87609ab](https://github.com/webtorrent/bittorrent-protocol/commit/87609abdf8223d4957d9f8c4dd5f06978092a68c)) 289 | -------------------------------------------------------------------------------- /test/protocol.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Protocol from '../index.js' 3 | 4 | test('Handshake', t => { 5 | t.plan(4) 6 | 7 | const wire = new Protocol() 8 | wire.on('error', err => { t.fail(err) }) 9 | wire.pipe(wire) 10 | 11 | wire.on('handshake', (infoHash, peerId) => { 12 | t.equal(Buffer.from(infoHash, 'hex').length, 20) 13 | t.equal(Buffer.from(infoHash, 'hex').toString(), '01234567890123456789') 14 | t.equal(Buffer.from(peerId, 'hex').length, 20) 15 | t.equal(Buffer.from(peerId, 'hex').toString(), '12345678901234567890') 16 | }) 17 | 18 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 19 | }) 20 | 21 | test('Handshake (with string args)', t => { 22 | t.plan(4) 23 | 24 | const wire = new Protocol() 25 | wire.on('error', err => { t.fail(err) }) 26 | wire.pipe(wire) 27 | 28 | wire.on('handshake', (infoHash, peerId) => { 29 | t.equal(Buffer.from(infoHash, 'hex').length, 20) 30 | t.equal(Buffer.from(infoHash, 'hex').toString(), '01234567890123456789') 31 | t.equal(Buffer.from(peerId, 'hex').length, 20) 32 | t.equal(Buffer.from(peerId, 'hex').toString(), '12345678901234567890') 33 | }) 34 | 35 | wire.handshake('3031323334353637383930313233343536373839', '3132333435363738393031323334353637383930') 36 | }) 37 | 38 | test('Asynchronous handshake + extended handshake', t => { 39 | const eventLog = [] 40 | 41 | const wire1 = new Protocol() // outgoing 42 | const wire2 = new Protocol() // incoming 43 | wire1.pipe(wire2).pipe(wire1) 44 | wire1.on('error', err => { t.fail(err) }) 45 | wire2.on('error', err => { t.fail(err) }) 46 | 47 | wire1.on('handshake', (infoHash, peerId, extensions) => { 48 | eventLog.push('w1 hs') 49 | t.equal(Buffer.from(infoHash, 'hex').toString(), '01234567890123456789') 50 | t.equal(Buffer.from(peerId, 'hex').toString(), '12345678901234567890') 51 | t.equal(extensions.extended, true) 52 | }) 53 | wire1.on('extended', (ext, obj) => { 54 | if (ext === 'handshake') { 55 | eventLog.push('w1 ex') 56 | t.ok(obj) 57 | } 58 | }) 59 | 60 | wire2.on('handshake', (infoHash, peerId, extensions) => { 61 | eventLog.push('w2 hs') 62 | t.equal(Buffer.from(infoHash, 'hex').toString(), '01234567890123456789') 63 | t.equal(Buffer.from(peerId, 'hex').toString(), '12345678901234567890') 64 | t.equal(extensions.extended, true) 65 | 66 | // Respond asynchronously 67 | queueMicrotask(() => { 68 | wire2.handshake(infoHash, peerId) 69 | }) 70 | }) 71 | wire2.on('extended', (ext, obj) => { 72 | if (ext === 'handshake') { 73 | eventLog.push('w2 ex') 74 | t.ok(obj) 75 | 76 | queueMicrotask(() => { 77 | // Last step: ensure handshakes came before extension protocol 78 | t.deepEqual(eventLog, ['w2 hs', 'w1 hs', 'w1 ex', 'w2 ex']) 79 | t.end() 80 | }) 81 | } 82 | }) 83 | 84 | wire1.handshake('3031323334353637383930313233343536373839', '3132333435363738393031323334353637383930') 85 | }) 86 | 87 | test('Unchoke', t => { 88 | t.plan(4) 89 | 90 | const wire = new Protocol() 91 | wire.on('error', err => { t.fail(err) }) 92 | wire.pipe(wire) 93 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 94 | 95 | t.ok(wire.amChoking) 96 | t.ok(wire.peerChoking) 97 | 98 | wire.on('unchoke', () => { 99 | t.ok(!wire.peerChoking) 100 | }) 101 | 102 | wire.unchoke() 103 | t.ok(!wire.amChoking) 104 | }) 105 | 106 | test('Interested', t => { 107 | t.plan(4) 108 | 109 | const wire = new Protocol() 110 | wire.on('error', err => { t.fail(err) }) 111 | wire.pipe(wire) 112 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 113 | 114 | t.ok(!wire.amInterested) 115 | t.ok(!wire.peerInterested) 116 | 117 | wire.on('interested', () => { 118 | t.ok(wire.peerInterested) 119 | }) 120 | 121 | wire.interested() 122 | t.ok(wire.amInterested) 123 | }) 124 | 125 | test('Request a piece', t => { 126 | t.plan(12) 127 | 128 | const wire = new Protocol() 129 | wire.on('error', err => { t.fail(err) }) 130 | wire.pipe(wire) 131 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890')) 132 | 133 | t.equal(wire.requests.length, 0) 134 | t.equal(wire.peerRequests.length, 0) 135 | 136 | wire.on('request', (i, offset, length, callback) => { 137 | t.equal(wire.requests.length, 1) 138 | t.equal(wire.peerRequests.length, 1) 139 | t.equal(i, 0) 140 | t.equal(offset, 1) 141 | t.equal(length, 11) 142 | callback(null, Buffer.from('hello world')) 143 | }) 144 | 145 | wire.once('unchoke', () => { 146 | t.equal(wire.requests.length, 0) 147 | wire.request(0, 1, 11, (err, buffer) => { 148 | t.equal(wire.requests.length, 0) 149 | t.ok(!err) 150 | t.equal(Buffer.from(buffer).toString(), 'hello world') 151 | }) 152 | t.equal(wire.requests.length, 1) 153 | }) 154 | 155 | wire.unchoke() 156 | }) 157 | 158 | test('No duplicate `have` events for same piece', t => { 159 | t.plan(6) 160 | 161 | const wire = new Protocol() 162 | wire.on('error', err => { t.fail(err) }) 163 | wire.pipe(wire) 164 | 165 | wire.handshake('3031323334353637383930313233343536373839', '3132333435363738393031323334353637383930') 166 | 167 | let haveEvents = 0 168 | wire.on('have', () => { 169 | haveEvents += 1 170 | }) 171 | t.equal(haveEvents, 0) 172 | t.equal(!!wire.peerPieces.get(0), false) 173 | wire.have(0) 174 | queueMicrotask(() => { 175 | t.equal(haveEvents, 1, 'emitted event for new piece') 176 | t.equal(!!wire.peerPieces.get(0), true) 177 | wire.have(0) 178 | queueMicrotask(() => { 179 | t.equal(haveEvents, 1, 'not emitted event for preexisting piece') 180 | t.equal(!!wire.peerPieces.get(0), true) 181 | }) 182 | }) 183 | }) 184 | 185 | test('Fast Extension: handshake when unsupported', t => { 186 | t.plan(4) 187 | 188 | const wire1 = new Protocol() 189 | const wire2 = new Protocol() 190 | wire1.pipe(wire2).pipe(wire1) 191 | wire1.on('error', err => { t.fail(err) }) 192 | wire2.on('error', err => { t.fail(err) }) 193 | 194 | wire1.on('handshake', (infoHash, peerId, extensions) => { 195 | t.equal(extensions.fast, false) 196 | t.equal(wire1.hasFast, false) 197 | t.equal(wire2.hasFast, false) 198 | }) 199 | 200 | wire2.on('handshake', (infoHash, peerId, extensions) => { 201 | t.equal(extensions.fast, true) 202 | // Respond asynchronously 203 | queueMicrotask(() => { 204 | wire2.handshake(infoHash, peerId, { fast: false }) // no support 205 | }) 206 | }) 207 | 208 | wire1.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 209 | }) 210 | 211 | test('Fast Extension: handshake when supported', t => { 212 | t.plan(4) 213 | 214 | const wire1 = new Protocol() 215 | const wire2 = new Protocol() 216 | wire1.pipe(wire2).pipe(wire1) 217 | wire1.on('error', err => { t.fail(err) }) 218 | wire2.on('error', err => { t.fail(err) }) 219 | 220 | wire1.on('handshake', (infoHash, peerId, extensions) => { 221 | t.equal(extensions.fast, true) 222 | t.equal(wire1.hasFast, true) 223 | t.equal(wire2.hasFast, true) 224 | }) 225 | 226 | wire2.on('handshake', (infoHash, peerId, extensions) => { 227 | t.equal(extensions.fast, true) 228 | // Respond asynchronously 229 | queueMicrotask(() => { 230 | wire2.handshake(infoHash, peerId, { fast: true }) 231 | }) 232 | }) 233 | 234 | wire1.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 235 | }) 236 | 237 | test('Fast Extension: have-all', t => { 238 | t.plan(2) 239 | 240 | const wire = new Protocol() 241 | wire.on('error', err => { t.fail(err) }) 242 | wire.pipe(wire) 243 | 244 | wire.once('handshake', (infoHash, peerId, extensions) => { 245 | t.equal(wire.hasFast, true) 246 | wire.haveAll() 247 | }) 248 | 249 | wire.once('have-all', () => { 250 | t.ok(true) 251 | }) 252 | 253 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 254 | }) 255 | 256 | test('Fast Extension: have-none', t => { 257 | t.plan(2) 258 | 259 | const wire = new Protocol() 260 | wire.on('error', err => { t.fail(err) }) 261 | wire.pipe(wire) 262 | 263 | wire.once('handshake', (infoHash, peerId, extensions) => { 264 | t.equal(wire.hasFast, true) 265 | wire.haveNone() 266 | }) 267 | 268 | wire.once('have-none', () => { 269 | t.ok(true) 270 | }) 271 | 272 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 273 | }) 274 | 275 | test('Fast Extension: suggest', t => { 276 | t.plan(2) 277 | 278 | const wire = new Protocol() 279 | wire.on('error', err => { t.fail(err) }) 280 | wire.pipe(wire) 281 | 282 | wire.once('handshake', (infoHash, peerId, extensions) => { 283 | t.equal(wire.hasFast, true) 284 | wire.suggest(42) 285 | }) 286 | 287 | wire.once('suggest', (index) => { 288 | t.equal(index, 42) 289 | }) 290 | 291 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 292 | }) 293 | 294 | test('Fast Extension: allowed-fast', t => { 295 | t.plan(6) 296 | 297 | const wire = new Protocol() 298 | wire.on('error', err => { t.fail(err) }) 299 | wire.pipe(wire) 300 | 301 | wire.once('handshake', (infoHash, peerId, extensions) => { 302 | t.equal(wire.hasFast, true) 303 | t.deepEqual(wire.allowedFastSet, []) 304 | wire.allowedFast(6) 305 | t.deepEqual(wire.allowedFastSet, [6]) 306 | t.deepEqual(wire.peerAllowedFastSet, []) 307 | }) 308 | 309 | wire.on('allowed-fast', (index) => { 310 | t.equal(index, 6) 311 | t.deepEqual(wire.peerAllowedFastSet, [6]) 312 | }) 313 | 314 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 315 | }) 316 | 317 | test('Fast Extension: reject on choke', t => { 318 | t.plan(14) 319 | 320 | const wire = new Protocol() 321 | wire.on('error', err => { t.fail(err) }) 322 | wire.pipe(wire) 323 | 324 | wire.once('handshake', (infoHash, peerId, extensions) => { 325 | t.equal(wire.extensions.fast, true) 326 | t.equal(wire.peerExtensions.fast, true) 327 | t.equal(wire.hasFast, true) 328 | wire.unchoke() 329 | }) 330 | 331 | wire.once('unchoke', () => { 332 | t.equal(wire.requests.length, 0) 333 | wire.request(0, 2, 22, (err, buffer) => { 334 | t.ok(err) 335 | }) 336 | t.equal(wire.requests.length, 1) 337 | t.equal(wire.peerRequests.length, 0) 338 | }) 339 | 340 | wire.on('request', (i, offset, length, callback) => { 341 | t.equal(wire.peerRequests.length, 1) 342 | t.equal(i, 0) 343 | t.equal(offset, 2) 344 | t.equal(length, 22) 345 | 346 | wire.choke() 347 | t.equal(wire.peerRequests.length, 0) // rejected 348 | }) 349 | 350 | wire.on('choke', () => { 351 | t.equal(wire.requests.length, 1) // not implicitly cancelled 352 | }) 353 | 354 | wire.on('reject', () => { 355 | t.equal(wire.requests.length, 0) 356 | }) 357 | 358 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 359 | }) 360 | 361 | test('Fast Extension: don\'t reject allowed-fast on choke', t => { 362 | t.plan(11) 363 | 364 | const wire = new Protocol() 365 | wire.on('error', err => { t.fail(err) }) 366 | wire.pipe(wire) 367 | 368 | wire.once('handshake', (infoHash, peerId, extensions) => { 369 | t.equal(wire.extensions.fast, true) 370 | t.equal(wire.peerExtensions.fast, true) 371 | t.equal(wire.hasFast, true) 372 | wire.allowedFast(6) 373 | wire.unchoke() 374 | }) 375 | 376 | wire.once('unchoke', () => { 377 | t.equal(wire.requests.length, 0) 378 | wire.request(6, 66, 666, (err, buffer) => { 379 | t.fail(err) 380 | }) 381 | t.equal(wire.requests.length, 1) 382 | t.equal(wire.peerRequests.length, 0) 383 | }) 384 | 385 | wire.on('request', (i, offset, length, callback) => { 386 | t.equal(wire.peerRequests.length, 1) 387 | t.equal(i, 6) 388 | t.equal(offset, 66) 389 | t.equal(length, 666) 390 | 391 | wire.choke() 392 | t.equal(wire.peerRequests.length, 1) // NOT rejected 393 | }) 394 | 395 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 396 | }) 397 | 398 | test('Fast Extension: reject on error', t => { 399 | t.plan(12) 400 | 401 | const wire = new Protocol() 402 | wire.on('error', err => { t.fail(err) }) 403 | wire.pipe(wire) 404 | 405 | wire.once('handshake', (infoHash, peerId, extensions) => { 406 | t.equal(wire.extensions.fast, true) 407 | t.equal(wire.peerExtensions.fast, true) 408 | t.equal(wire.hasFast, true) 409 | wire.unchoke() 410 | }) 411 | 412 | wire.once('unchoke', () => { 413 | t.equal(wire.requests.length, 0) 414 | wire.request(6, 66, 666, (err, buffer) => { 415 | t.ok(err) 416 | }) 417 | t.equal(wire.requests.length, 1) 418 | t.equal(wire.peerRequests.length, 0) 419 | }) 420 | 421 | wire.on('request', (i, offset, length, callback) => { 422 | t.equal(wire.peerRequests.length, 1) 423 | t.equal(i, 6) 424 | t.equal(offset, 66) 425 | t.equal(length, 666) 426 | callback(new Error('cannot satisfy'), null) 427 | }) 428 | 429 | wire.on('reject', () => { 430 | t.equal(wire.requests.length, 0) 431 | }) 432 | 433 | wire.handshake(Buffer.from('01234567890123456789'), Buffer.from('12345678901234567890'), { fast: true }) 434 | }) 435 | 436 | test('Fast Extension disabled: have-all', t => { 437 | t.plan(3) 438 | const wire = new Protocol() 439 | t.equal(wire.hasFast, false) 440 | t.throws(() => wire.haveAll()) 441 | wire.on('have-all', () => { t.fail() }) 442 | wire.on('close', () => { t.pass('wire closed') }) 443 | wire._onHaveAll() 444 | }) 445 | 446 | test('Fast Extension disabled: have-none', t => { 447 | t.plan(3) 448 | const wire = new Protocol() 449 | t.equal(wire.hasFast, false) 450 | t.throws(() => wire.haveNone()) 451 | wire.on('have-none', () => { t.fail() }) 452 | wire.on('close', () => { t.pass('wire closed') }) 453 | wire._onHaveNone() 454 | }) 455 | 456 | test('Fast Extension disabled: suggest', t => { 457 | t.plan(3) 458 | const wire = new Protocol() 459 | t.equal(wire.hasFast, false) 460 | t.throws(() => wire.suggest(42)) 461 | wire.on('suggest', () => { t.fail() }) 462 | wire.on('close', () => { t.pass('wire closed') }) 463 | wire._onSuggest(42) 464 | }) 465 | 466 | test('Fast Extension disabled: allowed-fast', t => { 467 | t.plan(3) 468 | const wire = new Protocol() 469 | t.equal(wire.hasFast, false) 470 | t.throws(() => wire.allowedFast(42)) 471 | wire.on('allowed-fast', () => { t.fail() }) 472 | wire.on('close', () => { t.pass('wire closed') }) 473 | wire._onAllowedFast(42) 474 | }) 475 | 476 | test('Fast Extension disabled: reject', t => { 477 | t.plan(3) 478 | const wire = new Protocol() 479 | t.equal(wire.hasFast, false) 480 | t.throws(() => wire.reject(42, 0, 99)) 481 | wire.on('reject', () => { t.fail() }) 482 | wire.on('close', () => { t.pass('wire closed') }) 483 | wire._onReject(42, 0, 99) 484 | }) 485 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! bittorrent-protocol. MIT License. WebTorrent LLC */ 2 | import bencode from 'bencode' 3 | import BitField from 'bitfield' 4 | import crypto from 'crypto' 5 | import Debug from 'debug' 6 | import RC4 from 'rc4' 7 | import { Duplex } from 'streamx' 8 | import { hash, concat, equal, hex2arr, arr2hex, text2arr, arr2text, randomBytes } from 'uint8-util' 9 | import throughput from 'throughput' 10 | import arrayRemove from 'unordered-array-remove' 11 | 12 | const debug = Debug('bittorrent-protocol') 13 | 14 | const BITFIELD_GROW = 400000 15 | const KEEP_ALIVE_TIMEOUT = 55000 16 | const ALLOWED_FAST_SET_MAX_LENGTH = 100 17 | 18 | const MESSAGE_PROTOCOL = text2arr('\u0013BitTorrent protocol') 19 | const MESSAGE_KEEP_ALIVE = new Uint8Array([0x00, 0x00, 0x00, 0x00]) 20 | const MESSAGE_CHOKE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x00]) 21 | const MESSAGE_UNCHOKE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x01]) 22 | const MESSAGE_INTERESTED = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x02]) 23 | const MESSAGE_UNINTERESTED = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x03]) 24 | 25 | const MESSAGE_RESERVED = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 26 | const MESSAGE_PORT = [0x00, 0x00, 0x00, 0x03, 0x09, 0x00, 0x00] 27 | 28 | // BEP6 Fast Extension 29 | const MESSAGE_HAVE_ALL = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x0E]) 30 | const MESSAGE_HAVE_NONE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x0F]) 31 | 32 | const DH_PRIME = 'ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a63a36210000000000090563' 33 | const DH_GENERATOR = 2 34 | const VC = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 35 | const CRYPTO_PROVIDE = new Uint8Array([0x00, 0x00, 0x01, 0x02]) 36 | const CRYPTO_SELECT = new Uint8Array([0x00, 0x00, 0x00, 0x02]) // always try to choose RC4 encryption instead of plaintext 37 | 38 | function xor (a, b) { 39 | for (let len = a.length; len--;) a[len] ^= b[len] 40 | return a 41 | } 42 | /** 43 | * @param {Uint8Array} buffer 44 | * @param {number} at 45 | * @returns number 46 | */ 47 | function getUint32 (buffer, at = 0) { 48 | return (buffer[at] << 24) | (buffer[at + 1] << 16) | (buffer[at + 2] << 8) | buffer[at + 3] 49 | } 50 | 51 | /** 52 | * @param {Uint8Array} buffer 53 | * @param {number} at 54 | * @param {number} value 55 | */ 56 | function setUint32 (buffer, at, value) { 57 | buffer[at] = (value >>> 24) & 0xFF 58 | buffer[at + 1] = (value >>> 16) & 0xFF 59 | buffer[at + 2] = (value >>> 8) & 0xFF 60 | buffer[at + 3] = value & 0xFF 61 | } 62 | 63 | class Request { 64 | constructor (piece, offset, length, callback) { 65 | this.piece = piece 66 | this.offset = offset 67 | this.length = length 68 | this.callback = callback 69 | } 70 | } 71 | 72 | class HaveAllBitField { 73 | constructor () { 74 | this.buffer = new Uint8Array() // dummy 75 | } 76 | 77 | get (index) { 78 | return true 79 | } 80 | 81 | set (index) {} 82 | } 83 | 84 | class Wire extends Duplex { 85 | constructor (type = null, retries = 0, peEnabled = false) { 86 | super() 87 | 88 | this._debugId = arr2hex(randomBytes(4)) 89 | this._debug('new wire') 90 | 91 | this.peerId = null // remote peer id (hex string) 92 | this.peerIdBuffer = null // remote peer id (buffer) 93 | this.type = type // connection type ('webrtc', 'tcpIncoming', 'tcpOutgoing', 'webSeed') 94 | 95 | this.amChoking = true // are we choking the peer? 96 | this.amInterested = false // are we interested in the peer? 97 | 98 | this.peerChoking = true // is the peer choking us? 99 | this.peerInterested = false // is the peer interested in us? 100 | 101 | // The largest torrent that I know of (the Geocities archive) is ~641 GB and has 102 | // ~41,000 pieces. Therefore, cap bitfield to 10x larger (400,000 bits) to support all 103 | // possible torrents but prevent malicious peers from growing bitfield to fill memory. 104 | this.peerPieces = new BitField(0, { grow: BITFIELD_GROW }) 105 | 106 | this.extensions = {} 107 | this.peerExtensions = {} 108 | 109 | this.requests = [] // outgoing 110 | this.peerRequests = [] // incoming 111 | 112 | this.extendedMapping = {} // number -> string, ex: 1 -> 'ut_metadata' 113 | this.peerExtendedMapping = {} // string -> number, ex: 9 -> 'ut_metadata' 114 | 115 | // The extended handshake to send, minus the "m" field, which gets automatically 116 | // filled from `this.extendedMapping` 117 | this.extendedHandshake = {} 118 | 119 | this.peerExtendedHandshake = {} // remote peer's extended handshake 120 | 121 | // BEP6 Fast Estension 122 | this.hasFast = false // is fast extension enabled? 123 | this.allowedFastSet = [] // allowed fast set 124 | this.peerAllowedFastSet = [] // peer's allowed fast set 125 | 126 | this._ext = {} // string -> function, ex 'ut_metadata' -> ut_metadata() 127 | this._nextExt = 1 128 | 129 | this.uploaded = 0 130 | this.downloaded = 0 131 | this.uploadSpeed = throughput() 132 | this.downloadSpeed = throughput() 133 | 134 | this._keepAliveInterval = null 135 | this._timeout = null 136 | this._timeoutMs = 0 137 | this._timeoutExpiresAt = null 138 | 139 | this._finished = false 140 | 141 | this._parserSize = 0 // number of needed bytes to parse next message from remote peer 142 | this._parser = null // function to call once `this._parserSize` bytes are available 143 | 144 | this._buffer = [] // incomplete message data 145 | this._bufferSize = 0 // cached total length of buffers in `this._buffer` 146 | 147 | this._peEnabled = peEnabled 148 | if (peEnabled) { 149 | this._dh = crypto.createDiffieHellman(DH_PRIME, 'hex', DH_GENERATOR) // crypto object used to generate keys/secret 150 | this._myPubKey = this._dh.generateKeys('hex') // my DH public key 151 | } else { 152 | this._myPubKey = null 153 | } 154 | this._peerPubKey = null // peer's DH public key 155 | this._sharedSecret = null // shared DH secret 156 | this._peerCryptoProvide = [] // encryption methods provided by peer; we expect this to always contain 0x02 157 | this._cryptoHandshakeDone = false 158 | 159 | this._cryptoSyncPattern = null // the pattern to search for when resynchronizing after receiving pe1/pe2 160 | this._waitMaxBytes = null // the maximum number of bytes resynchronization must occur within 161 | this._encryptionMethod = null // 1 for plaintext, 2 for RC4 162 | this._encryptGenerator = null // RC4 keystream generator for encryption 163 | this._decryptGenerator = null // RC4 keystream generator for decryption 164 | this._setGenerators = false // a flag for whether setEncrypt() has successfully completed 165 | 166 | this.once('finish', () => this._onFinish()) 167 | 168 | this.on('finish', this._onFinish) 169 | this._debug('type:', this.type) 170 | 171 | if (this.type === 'tcpIncoming' && this._peEnabled) { 172 | // If we are not the initiator, we should wait to see if the client begins 173 | // with PE/MSE handshake or the standard bittorrent handshake. 174 | this._determineHandshakeType() 175 | } else if (this.type === 'tcpOutgoing' && this._peEnabled && retries === 0) { 176 | this._parsePe2() 177 | } else { 178 | this._parseHandshake(null) 179 | } 180 | } 181 | 182 | /** 183 | * Set whether to send a "keep-alive" ping (sent every 55s) 184 | * @param {boolean} enable 185 | */ 186 | setKeepAlive (enable) { 187 | this._debug('setKeepAlive %s', enable) 188 | clearInterval(this._keepAliveInterval) 189 | if (enable === false) return 190 | this._keepAliveInterval = setInterval(() => { 191 | this.keepAlive() 192 | }, KEEP_ALIVE_TIMEOUT) 193 | } 194 | 195 | /** 196 | * Set the amount of time to wait before considering a request to be "timed out" 197 | * @param {number} ms 198 | * @param {boolean=} unref (should the timer be unref'd? default: false) 199 | */ 200 | setTimeout (ms, unref) { 201 | this._debug('setTimeout ms=%d unref=%s', ms, unref) 202 | this._timeoutMs = ms 203 | this._timeoutUnref = !!unref 204 | this._resetTimeout(true) 205 | } 206 | 207 | destroy () { 208 | if (this.destroyed) return 209 | this._debug('destroy') 210 | this.end() 211 | return this 212 | } 213 | 214 | end (data) { 215 | if (this.destroyed || this.destroying) return 216 | this._debug('end') 217 | this._onUninterested() 218 | this._onChoke() 219 | return super.end(data) 220 | } 221 | 222 | /** 223 | * Use the specified protocol extension. 224 | * @param {function} Extension 225 | */ 226 | use (Extension) { 227 | const name = Extension.prototype.name 228 | if (!name) { 229 | throw new Error('Extension class requires a "name" property on the prototype') 230 | } 231 | this._debug('use extension.name=%s', name) 232 | 233 | const ext = this._nextExt 234 | const handler = new Extension(this) 235 | 236 | function noop () {} 237 | 238 | if (typeof handler.onHandshake !== 'function') { 239 | handler.onHandshake = noop 240 | } 241 | if (typeof handler.onExtendedHandshake !== 'function') { 242 | handler.onExtendedHandshake = noop 243 | } 244 | if (typeof handler.onMessage !== 'function') { 245 | handler.onMessage = noop 246 | } 247 | 248 | this.extendedMapping[ext] = name 249 | this._ext[name] = handler 250 | this[name] = handler 251 | 252 | this._nextExt += 1 253 | } 254 | 255 | // 256 | // OUTGOING MESSAGES 257 | // 258 | 259 | /** 260 | * Message "keep-alive": 261 | */ 262 | keepAlive () { 263 | this._debug('keep-alive') 264 | this._push(MESSAGE_KEEP_ALIVE) 265 | } 266 | 267 | sendPe1 () { 268 | if (this._peEnabled) { 269 | const padALen = Math.floor(Math.random() * 513) 270 | const padA = randomBytes(padALen) 271 | this._push(concat([hex2arr(this._myPubKey), padA])) 272 | } 273 | } 274 | 275 | sendPe2 () { 276 | const padBLen = Math.floor(Math.random() * 513) 277 | const padB = randomBytes(padBLen) 278 | this._push(concat([hex2arr(this._myPubKey), padB])) 279 | } 280 | 281 | async sendPe3 (infoHash) { 282 | await this.setEncrypt(this._sharedSecret, infoHash) 283 | 284 | const hash1Buffer = await hash(hex2arr(this._utfToHex('req1') + this._sharedSecret)) 285 | 286 | const hash2Buffer = await hash(hex2arr(this._utfToHex('req2') + infoHash)) 287 | const hash3Buffer = await hash(hex2arr(this._utfToHex('req3') + this._sharedSecret)) 288 | const hashesXorBuffer = xor(hash2Buffer, hash3Buffer) 289 | 290 | const padCLen = new DataView(randomBytes(2).buffer).getUint16(0) % 512 291 | const padCBuffer = randomBytes(padCLen) 292 | 293 | let vcAndProvideBuffer = new Uint8Array(8 + 4 + 2 + padCLen + 2) 294 | vcAndProvideBuffer.set(VC) 295 | vcAndProvideBuffer.set(CRYPTO_PROVIDE, 8) 296 | 297 | const view = new DataView(vcAndProvideBuffer.buffer) 298 | 299 | view.setInt16(12, padCLen) // pad C length 300 | padCBuffer.copy(vcAndProvideBuffer, 14) 301 | view.setInt16(14 + padCLen, 0) // IA length 302 | vcAndProvideBuffer = this._encryptHandshake(vcAndProvideBuffer) 303 | 304 | this._push(concat([hash1Buffer, hashesXorBuffer, vcAndProvideBuffer])) 305 | } 306 | 307 | async sendPe4 (infoHash) { 308 | await this.setEncrypt(this._sharedSecret, infoHash) 309 | 310 | const padDLen = new DataView(randomBytes(2).buffer).getUint16(0) % 512 311 | const padDBuffer = randomBytes(padDLen) 312 | let vcAndSelectBuffer = new Uint8Array(8 + 4 + 2 + padDLen) 313 | const view = new DataView(vcAndSelectBuffer.buffer) 314 | 315 | vcAndSelectBuffer.set(VC) 316 | vcAndSelectBuffer.set(CRYPTO_SELECT, 8) 317 | view.setInt16(12, padDLen) // lenD? 318 | vcAndSelectBuffer.set(padDBuffer, 14) 319 | vcAndSelectBuffer = this._encryptHandshake(vcAndSelectBuffer) 320 | this._push(vcAndSelectBuffer) 321 | this._cryptoHandshakeDone = true 322 | this._debug('completed crypto handshake') 323 | } 324 | 325 | /** 326 | * Message: "handshake" 327 | * @param {Uint8Array|string} infoHash (as Buffer or *hex* string) 328 | * @param {Uint8Array|string} peerId 329 | * @param {Object} extensions 330 | */ 331 | handshake (infoHash, peerId, extensions) { 332 | let infoHashBuffer 333 | let peerIdBuffer 334 | if (typeof infoHash === 'string') { 335 | infoHash = infoHash.toLowerCase() 336 | infoHashBuffer = hex2arr(infoHash) 337 | } else { 338 | infoHashBuffer = infoHash 339 | infoHash = arr2hex(infoHashBuffer) 340 | } 341 | if (typeof peerId === 'string') { 342 | peerIdBuffer = hex2arr(peerId) 343 | } else { 344 | peerIdBuffer = peerId 345 | peerId = arr2hex(peerIdBuffer) 346 | } 347 | 348 | this._infoHash = infoHashBuffer 349 | 350 | if (infoHashBuffer.length !== 20 || peerIdBuffer.length !== 20) { 351 | throw new Error('infoHash and peerId MUST have length 20') 352 | } 353 | 354 | this._debug('handshake i=%s p=%s exts=%o', infoHash, peerId, extensions) 355 | 356 | const reserved = new Uint8Array(MESSAGE_RESERVED) 357 | 358 | this.extensions = { 359 | extended: true, 360 | dht: !!(extensions && extensions.dht), 361 | fast: !!(extensions && extensions.fast) 362 | } 363 | 364 | reserved[5] |= 0x10 // enable extended message 365 | if (this.extensions.dht) reserved[7] |= 0x01 366 | if (this.extensions.fast) reserved[7] |= 0x04 367 | 368 | // BEP6 Fast Extension: The extension is enabled only if both ends of the connection set this bit. 369 | if (this.extensions.fast && this.peerExtensions.fast) { 370 | this._debug('fast extension is enabled') 371 | this.hasFast = true 372 | } 373 | this._push(concat([MESSAGE_PROTOCOL, reserved, infoHashBuffer, peerIdBuffer])) 374 | this._handshakeSent = true 375 | 376 | if (this.peerExtensions.extended && !this._extendedHandshakeSent) { 377 | // Peer's handshake indicated support already 378 | // (incoming connection) 379 | this._sendExtendedHandshake() 380 | } 381 | } 382 | 383 | /* Peer supports BEP-0010, send extended handshake. 384 | * 385 | * This comes after the 'handshake' event to give the user a chance to populate 386 | * `this.extendedHandshake` and `this.extendedMapping` before the extended handshake 387 | * is sent to the remote peer. 388 | */ 389 | _sendExtendedHandshake () { 390 | // Create extended message object from registered extensions 391 | const msg = Object.assign({}, this.extendedHandshake) 392 | msg.m = {} 393 | for (const ext in this.extendedMapping) { 394 | const name = this.extendedMapping[ext] 395 | msg.m[name] = Number(ext) 396 | } 397 | 398 | // Send extended handshake 399 | this.extended(0, bencode.encode(msg)) 400 | this._extendedHandshakeSent = true 401 | } 402 | 403 | /** 404 | * Message "choke": 405 | */ 406 | choke () { 407 | if (this.amChoking) return 408 | this.amChoking = true 409 | this._debug('choke') 410 | this._push(MESSAGE_CHOKE) 411 | 412 | if (this.hasFast) { 413 | // BEP6: If a peer sends a choke, it MUST reject all requests from the peer to whom the choke 414 | // was sent except it SHOULD NOT reject requests for pieces that are in the allowed fast set. 415 | let allowedCount = 0 416 | while (this.peerRequests.length > allowedCount) { // until only allowed requests are left 417 | const request = this.peerRequests[allowedCount] // first non-allowed request 418 | if (this.allowedFastSet.includes(request.piece)) { 419 | ++allowedCount // count request as allowed 420 | } else { 421 | this.reject(request.piece, request.offset, request.length) // removes from this.peerRequests 422 | } 423 | } 424 | } else { 425 | while (this.peerRequests.length) { 426 | this.peerRequests.pop() 427 | } 428 | } 429 | } 430 | 431 | /** 432 | * Message "unchoke": 433 | */ 434 | unchoke () { 435 | if (!this.amChoking) return 436 | this.amChoking = false 437 | this._debug('unchoke') 438 | this._push(MESSAGE_UNCHOKE) 439 | } 440 | 441 | /** 442 | * Message "interested": 443 | */ 444 | interested () { 445 | if (this.amInterested) return 446 | this.amInterested = true 447 | this._debug('interested') 448 | this._push(MESSAGE_INTERESTED) 449 | } 450 | 451 | /** 452 | * Message "uninterested": 453 | */ 454 | uninterested () { 455 | if (!this.amInterested) return 456 | this.amInterested = false 457 | this._debug('uninterested') 458 | this._push(MESSAGE_UNINTERESTED) 459 | } 460 | 461 | /** 462 | * Message "have": 463 | * @param {number} index 464 | */ 465 | have (index) { 466 | this._debug('have %d', index) 467 | this._message(4, [index], null) 468 | } 469 | 470 | /** 471 | * Message "bitfield": 472 | * @param {BitField|Buffer} bitfield 473 | */ 474 | bitfield (bitfield) { 475 | this._debug('bitfield') 476 | if (!ArrayBuffer.isView(bitfield)) bitfield = bitfield.buffer 477 | this._message(5, [], bitfield) 478 | } 479 | 480 | /** 481 | * Message "request": 482 | * @param {number} index 483 | * @param {number} offset 484 | * @param {number} length 485 | * @param {function} cb 486 | */ 487 | request (index, offset, length, cb) { 488 | if (!cb) cb = () => {} 489 | if (this._finished) return cb(new Error('wire is closed')) 490 | 491 | if (this.peerChoking && !(this.hasFast && this.peerAllowedFastSet.includes(index))) { 492 | return cb(new Error('peer is choking')) 493 | } 494 | 495 | this._debug('request index=%d offset=%d length=%d', index, offset, length) 496 | 497 | this.requests.push(new Request(index, offset, length, cb)) 498 | if (!this._timeout) { 499 | this._resetTimeout(true) 500 | } 501 | this._message(6, [index, offset, length], null) 502 | } 503 | 504 | /** 505 | * Message "piece": 506 | * @param {number} index 507 | * @param {number} offset 508 | * @param {Uint8Array} buffer 509 | */ 510 | piece (index, offset, buffer) { 511 | this._debug('piece index=%d offset=%d', index, offset) 512 | this._message(7, [index, offset], buffer) 513 | this.uploaded += buffer.length 514 | this.uploadSpeed(buffer.length) 515 | this.emit('upload', buffer.length) 516 | } 517 | 518 | /** 519 | * Message "cancel": 520 | * @param {number} index 521 | * @param {number} offset 522 | * @param {number} length 523 | */ 524 | cancel (index, offset, length) { 525 | this._debug('cancel index=%d offset=%d length=%d', index, offset, length) 526 | this._callback( 527 | this._pull(this.requests, index, offset, length), 528 | new Error('request was cancelled'), 529 | null 530 | ) 531 | this._message(8, [index, offset, length], null) 532 | } 533 | 534 | /** 535 | * Message: "port" 536 | * @param {Number} port 537 | */ 538 | port (port) { 539 | this._debug('port %d', port) 540 | const message = new Uint8Array(MESSAGE_PORT) 541 | const view = new DataView(message.buffer) 542 | view.setUint16(5, port) 543 | this._push(message) 544 | } 545 | 546 | /** 547 | * Message: "suggest" (BEP6) 548 | * @param {number} index 549 | */ 550 | suggest (index) { 551 | if (!this.hasFast) throw Error('fast extension is disabled') 552 | this._debug('suggest %d', index) 553 | this._message(0x0D, [index], null) 554 | } 555 | 556 | /** 557 | * Message: "have-all" (BEP6) 558 | */ 559 | haveAll () { 560 | if (!this.hasFast) throw Error('fast extension is disabled') 561 | this._debug('have-all') 562 | this._push(MESSAGE_HAVE_ALL) 563 | } 564 | 565 | /** 566 | * Message: "have-none" (BEP6) 567 | */ 568 | haveNone () { 569 | if (!this.hasFast) throw Error('fast extension is disabled') 570 | this._debug('have-none') 571 | this._push(MESSAGE_HAVE_NONE) 572 | } 573 | 574 | /** 575 | * Message "reject": (BEP6) 576 | * @param {number} index 577 | * @param {number} offset 578 | * @param {number} length 579 | */ 580 | reject (index, offset, length) { 581 | if (!this.hasFast) throw Error('fast extension is disabled') 582 | this._debug('reject index=%d offset=%d length=%d', index, offset, length) 583 | this._pull(this.peerRequests, index, offset, length) 584 | this._message(0x10, [index, offset, length], null) 585 | } 586 | 587 | /** 588 | * Message: "allowed-fast" (BEP6) 589 | * @param {number} index 590 | */ 591 | allowedFast (index) { 592 | if (!this.hasFast) throw Error('fast extension is disabled') 593 | this._debug('allowed-fast %d', index) 594 | if (!this.allowedFastSet.includes(index)) this.allowedFastSet.push(index) 595 | this._message(0x11, [index], null) 596 | } 597 | 598 | /** 599 | * Message: "extended" 600 | * @param {number|string} ext 601 | * @param {Object} obj 602 | */ 603 | extended (ext, obj) { 604 | this._debug('extended ext=%s', ext) 605 | if (typeof ext === 'string' && this.peerExtendedMapping[ext]) { 606 | ext = this.peerExtendedMapping[ext] 607 | } 608 | if (typeof ext === 'number') { 609 | const extId = new Uint8Array([ext]) 610 | const buf = ArrayBuffer.isView(obj) ? obj : bencode.encode(obj) 611 | 612 | this._message(20, [], concat([extId, buf])) 613 | } else { 614 | throw new Error(`Unrecognized extension: ${ext}`) 615 | } 616 | } 617 | 618 | /** 619 | * Sets the encryption method for this wire, as per PSE/ME specification 620 | * 621 | * @param {string} sharedSecret: A hex-encoded string, which is the shared secret agreed 622 | * upon from DH key exchange 623 | * @param {string} infoHash: A hex-encoded info hash 624 | * @returns boolean, true if encryption setting succeeds, false if it fails. 625 | */ 626 | async setEncrypt (sharedSecret, infoHash) { 627 | if (!this.type.startsWith('tcp')) return false 628 | 629 | const outgoing = this.type === 'tcpOutgoing' 630 | 631 | const keyAGenerator = new RC4([...await hash(hex2arr(this._utfToHex('keyA') + sharedSecret + infoHash))]) 632 | const keyBGenerator = new RC4([...await hash(hex2arr(this._utfToHex('keyB') + sharedSecret + infoHash))]) 633 | 634 | this._encryptGenerator = outgoing ? keyAGenerator : keyBGenerator 635 | this._decryptGenerator = outgoing ? keyBGenerator : keyAGenerator 636 | 637 | // Discard the first 1024 bytes, as per MSE/PE implementation 638 | for (let i = 0; i < 1024; i++) { 639 | this._encryptGenerator.randomByte() 640 | this._decryptGenerator.randomByte() 641 | } 642 | 643 | this._setGenerators = true 644 | this.emit('_generators') 645 | return true 646 | } 647 | 648 | /** 649 | * Send a message to the remote peer. 650 | */ 651 | _message (id, numbers, data) { 652 | const dataLength = data ? data.length : 0 653 | const buffer = new Uint8Array(5 + (4 * numbers.length)) 654 | 655 | setUint32(buffer, 0, buffer.length + dataLength - 4) 656 | buffer[4] = id 657 | for (let i = 0; i < numbers.length; i++) { 658 | setUint32(buffer, 5 + (4 * i), numbers[i]) 659 | } 660 | 661 | this._push(buffer) 662 | if (data) this._push(data) 663 | } 664 | 665 | _push (data) { 666 | if (this._finished) return 667 | if (this._encryptionMethod === 2 && this._cryptoHandshakeDone) { 668 | data = this._encrypt(data) 669 | } 670 | return this.push(data) 671 | } 672 | 673 | // 674 | // INCOMING MESSAGES 675 | // 676 | 677 | _onKeepAlive () { 678 | this._debug('got keep-alive') 679 | this.emit('keep-alive') 680 | } 681 | 682 | _onPe1 (pubKeyBuffer) { 683 | this._peerPubKey = arr2hex(pubKeyBuffer) 684 | this._sharedSecret = this._dh.computeSecret(this._peerPubKey, 'hex', 'hex') 685 | this.emit('pe1') 686 | } 687 | 688 | _onPe2 (pubKeyBuffer) { 689 | this._peerPubKey = arr2hex(pubKeyBuffer) 690 | this._sharedSecret = this._dh.computeSecret(this._peerPubKey, 'hex', 'hex') 691 | this.emit('pe2') 692 | } 693 | 694 | async _onPe3 (hashesXorBuffer) { 695 | const hash3 = await hash(hex2arr(this._utfToHex('req3') + this._sharedSecret)) 696 | const sKeyHash = arr2hex(xor(hash3, hashesXorBuffer)) 697 | this.emit('pe3', sKeyHash) 698 | } 699 | 700 | _onPe3Encrypted (vcBuffer, peerProvideBuffer) { 701 | if (!equal(vcBuffer, VC)) { 702 | this._debug('Error: verification constant did not match') 703 | this.destroy() 704 | return 705 | } 706 | 707 | for (const provideByte of peerProvideBuffer.values()) { 708 | if (provideByte !== 0) { 709 | this._peerCryptoProvide.push(provideByte) 710 | } 711 | } 712 | if (this._peerCryptoProvide.includes(2)) { 713 | this._encryptionMethod = 2 714 | } else { 715 | this._debug('Error: RC4 encryption method not provided by peer') 716 | this.destroy() 717 | } 718 | } 719 | 720 | _onPe4 (peerSelectBuffer) { 721 | this._encryptionMethod = peerSelectBuffer[3] 722 | if (!CRYPTO_PROVIDE.includes(this._encryptionMethod)) { 723 | this._debug('Error: peer selected invalid crypto method') 724 | this.destroy() 725 | } 726 | this._cryptoHandshakeDone = true 727 | this._debug('crypto handshake done') 728 | this.emit('pe4') 729 | } 730 | 731 | _onHandshake (infoHashBuffer, peerIdBuffer, extensions) { 732 | const infoHash = arr2hex(infoHashBuffer) 733 | const peerId = arr2hex(peerIdBuffer) 734 | 735 | this._debug('got handshake i=%s p=%s exts=%o', infoHash, peerId, extensions) 736 | 737 | this.peerId = peerId 738 | this.peerIdBuffer = peerIdBuffer 739 | this.peerExtensions = extensions 740 | 741 | // BEP6 Fast Extension: The extension is enabled only if both ends of the connection set this bit. 742 | if (this.extensions.fast && this.peerExtensions.fast) { 743 | this._debug('fast extension is enabled') 744 | this.hasFast = true 745 | } 746 | 747 | this.emit('handshake', infoHash, peerId, extensions) 748 | 749 | for (const name in this._ext) { 750 | this._ext[name].onHandshake(infoHash, peerId, extensions) 751 | } 752 | 753 | if (extensions.extended && this._handshakeSent && 754 | !this._extendedHandshakeSent) { 755 | // outgoing connection 756 | this._sendExtendedHandshake() 757 | } 758 | } 759 | 760 | _onChoke () { 761 | this.peerChoking = true 762 | this._debug('got choke') 763 | this.emit('choke') 764 | if (!this.hasFast) { 765 | // BEP6 Fast Extension: Choke no longer implicitly rejects all pending requests 766 | while (this.requests.length) { 767 | this._callback(this.requests.pop(), new Error('peer is choking'), null) 768 | } 769 | } 770 | } 771 | 772 | _onUnchoke () { 773 | this.peerChoking = false 774 | this._debug('got unchoke') 775 | this.emit('unchoke') 776 | } 777 | 778 | _onInterested () { 779 | this.peerInterested = true 780 | this._debug('got interested') 781 | this.emit('interested') 782 | } 783 | 784 | _onUninterested () { 785 | this.peerInterested = false 786 | this._debug('got uninterested') 787 | this.emit('uninterested') 788 | } 789 | 790 | _onHave (index) { 791 | if (this.peerPieces.get(index)) return 792 | this._debug('got have %d', index) 793 | 794 | this.peerPieces.set(index, true) 795 | this.emit('have', index) 796 | } 797 | 798 | _onBitField (buffer) { 799 | this.peerPieces = new BitField(buffer) 800 | this._debug('got bitfield') 801 | this.emit('bitfield', this.peerPieces) 802 | } 803 | 804 | _onRequest (index, offset, length) { 805 | if (this.amChoking && !(this.hasFast && this.allowedFastSet.includes(index))) { 806 | // BEP6: If a peer receives a request from a peer its choking, the peer receiving 807 | // the request SHOULD send a reject unless the piece is in the allowed fast set. 808 | if (this.hasFast) this.reject(index, offset, length) 809 | return 810 | } 811 | this._debug('got request index=%d offset=%d length=%d', index, offset, length) 812 | 813 | const respond = (err, buffer) => { 814 | if (request !== this._pull(this.peerRequests, index, offset, length)) return 815 | if (err) { 816 | this._debug('error satisfying request index=%d offset=%d length=%d (%s)', index, offset, length, err.message) 817 | if (this.hasFast) this.reject(index, offset, length) 818 | return 819 | } 820 | this.piece(index, offset, buffer) 821 | } 822 | 823 | const request = new Request(index, offset, length, respond) 824 | this.peerRequests.push(request) 825 | this.emit('request', index, offset, length, respond) 826 | } 827 | 828 | _onPiece (index, offset, buffer) { 829 | this._debug('got piece index=%d offset=%d', index, offset) 830 | this._callback(this._pull(this.requests, index, offset, buffer.length), null, buffer) 831 | this.downloaded += buffer.length 832 | this.downloadSpeed(buffer.length) 833 | this.emit('download', buffer.length) 834 | this.emit('piece', index, offset, buffer) 835 | } 836 | 837 | _onCancel (index, offset, length) { 838 | this._debug('got cancel index=%d offset=%d length=%d', index, offset, length) 839 | this._pull(this.peerRequests, index, offset, length) 840 | this.emit('cancel', index, offset, length) 841 | } 842 | 843 | _onPort (port) { 844 | this._debug('got port %d', port) 845 | this.emit('port', port) 846 | } 847 | 848 | _onSuggest (index) { 849 | if (!this.hasFast) { 850 | // BEP6: the peer MUST close the connection 851 | this._debug('Error: got suggest whereas fast extension is disabled') 852 | this.destroy() 853 | return 854 | } 855 | this._debug('got suggest %d', index) 856 | this.emit('suggest', index) 857 | } 858 | 859 | _onHaveAll () { 860 | if (!this.hasFast) { 861 | // BEP6: the peer MUST close the connection 862 | this._debug('Error: got have-all whereas fast extension is disabled') 863 | this.destroy() 864 | return 865 | } 866 | this._debug('got have-all') 867 | this.peerPieces = new HaveAllBitField() 868 | this.emit('have-all') 869 | } 870 | 871 | _onHaveNone () { 872 | if (!this.hasFast) { 873 | // BEP6: the peer MUST close the connection 874 | this._debug('Error: got have-none whereas fast extension is disabled') 875 | this.destroy() 876 | return 877 | } 878 | this._debug('got have-none') 879 | this.emit('have-none') 880 | } 881 | 882 | _onReject (index, offset, length) { 883 | if (!this.hasFast) { 884 | // BEP6: the peer MUST close the connection 885 | this._debug('Error: got reject whereas fast extension is disabled') 886 | this.destroy() 887 | return 888 | } 889 | this._debug('got reject index=%d offset=%d length=%d', index, offset, length) 890 | this._callback( 891 | this._pull(this.requests, index, offset, length), 892 | new Error('request was rejected'), 893 | null 894 | ) 895 | this.emit('reject', index, offset, length) 896 | } 897 | 898 | _onAllowedFast (index) { 899 | if (!this.hasFast) { 900 | // BEP6: the peer MUST close the connection 901 | this._debug('Error: got allowed-fast whereas fast extension is disabled') 902 | this.destroy() 903 | return 904 | } 905 | this._debug('got allowed-fast %d', index) 906 | if (!this.peerAllowedFastSet.includes(index)) this.peerAllowedFastSet.push(index) 907 | if (this.peerAllowedFastSet.length > ALLOWED_FAST_SET_MAX_LENGTH) this.peerAllowedFastSet.shift() 908 | this.emit('allowed-fast', index) 909 | } 910 | 911 | _onExtended (ext, buf) { 912 | if (ext === 0) { 913 | let info 914 | try { 915 | info = bencode.decode(buf) 916 | } catch (err) { 917 | this._debug('ignoring invalid extended handshake: %s', err.message || err) 918 | } 919 | 920 | if (!info) return 921 | this.peerExtendedHandshake = info 922 | 923 | if (typeof info.m === 'object') { 924 | for (const name in info.m) { 925 | this.peerExtendedMapping[name] = Number(info.m[name].toString()) 926 | } 927 | } 928 | for (const name in this._ext) { 929 | if (this.peerExtendedMapping[name]) { 930 | this._ext[name].onExtendedHandshake(this.peerExtendedHandshake) 931 | } 932 | } 933 | this._debug('got extended handshake') 934 | this.emit('extended', 'handshake', this.peerExtendedHandshake) 935 | } else { 936 | if (this.extendedMapping[ext]) { 937 | ext = this.extendedMapping[ext] // friendly name for extension 938 | if (this._ext[ext]) { 939 | // there is an registered extension handler, so call it 940 | this._ext[ext].onMessage(buf) 941 | } 942 | } 943 | this._debug('got extended message ext=%s', ext) 944 | this.emit('extended', ext, buf) 945 | } 946 | } 947 | 948 | _onTimeout () { 949 | this._debug('request timed out') 950 | this._callback(this.requests.shift(), new Error('request has timed out'), null) 951 | this.emit('timeout') 952 | } 953 | 954 | /** 955 | * Duplex stream method. Called whenever the remote peer has data for us. Data that the 956 | * remote peer sends gets buffered (i.e. not actually processed) until the right number 957 | * of bytes have arrived, determined by the last call to `this._parse(number, callback)`. 958 | * Once enough bytes have arrived to process the message, the callback function 959 | * (i.e. `this._parser`) gets called with the full buffer of data. 960 | * @param {Uint8Array} data 961 | * @param {function} cb 962 | */ 963 | _write (data, cb) { 964 | if (this._encryptionMethod === 2 && this._cryptoHandshakeDone) { 965 | data = this._decrypt(data) 966 | } 967 | this._bufferSize += data.length 968 | this._buffer.push(data) 969 | if (this._buffer.length > 1) { 970 | this._buffer = [concat(this._buffer, this._bufferSize)] 971 | } 972 | // now this._buffer is an array containing a single Buffer 973 | if (this._cryptoSyncPattern) { 974 | const index = this._buffer[0].indexOf(this._cryptoSyncPattern) 975 | if (index !== -1) { 976 | this._buffer[0] = this._buffer[0].slice(index + this._cryptoSyncPattern.length) 977 | this._bufferSize -= (index + this._cryptoSyncPattern.length) 978 | this._cryptoSyncPattern = null 979 | } else if (this._bufferSize + data.length > this._waitMaxBytes + this._cryptoSyncPattern.length) { 980 | this._debug('Error: could not resynchronize') 981 | this.destroy() 982 | return 983 | } 984 | } 985 | 986 | while (this._bufferSize >= this._parserSize && !this._cryptoSyncPattern) { 987 | if (this._parserSize === 0) { 988 | this._parser(new Uint8Array()) 989 | } else { 990 | const buffer = this._buffer[0] 991 | 992 | this._bufferSize -= this._parserSize 993 | this._buffer = this._bufferSize 994 | ? [buffer.subarray(this._parserSize)] 995 | : [] 996 | this._parser(buffer.subarray(0, this._parserSize)) 997 | } 998 | } 999 | 1000 | cb(null) // Signal that we're ready for more data 1001 | } 1002 | 1003 | _callback (request, err, buffer) { 1004 | if (!request) return 1005 | 1006 | this._resetTimeout(!this.peerChoking && !this._finished) 1007 | 1008 | request.callback(err, buffer) 1009 | } 1010 | 1011 | _resetTimeout (setAgain) { 1012 | if (!setAgain || !this._timeoutMs || !this.requests.length) { 1013 | clearTimeout(this._timeout) 1014 | this._timeout = null 1015 | this._timeoutExpiresAt = null 1016 | return 1017 | } 1018 | 1019 | const timeoutExpiresAt = Date.now() + this._timeoutMs 1020 | 1021 | if (this._timeout) { 1022 | // If existing expiration is already within 5% of correct, it's close enough 1023 | if (timeoutExpiresAt - this._timeoutExpiresAt < this._timeoutMs * 0.05) { 1024 | return 1025 | } 1026 | clearTimeout(this._timeout) 1027 | } 1028 | 1029 | this._timeoutExpiresAt = timeoutExpiresAt 1030 | this._timeout = setTimeout(() => this._onTimeout(), this._timeoutMs) 1031 | if (this._timeoutUnref && this._timeout.unref) this._timeout.unref() 1032 | } 1033 | 1034 | /** 1035 | * Takes a number of bytes that the local peer is waiting to receive from the remote peer 1036 | * in order to parse a complete message, and a callback function to be called once enough 1037 | * bytes have arrived. 1038 | * @param {number} size 1039 | * @param {function} parser 1040 | */ 1041 | _parse (size, parser) { 1042 | this._parserSize = size 1043 | this._parser = parser 1044 | } 1045 | 1046 | _parseUntil (pattern, maxBytes) { 1047 | this._cryptoSyncPattern = pattern 1048 | this._waitMaxBytes = maxBytes 1049 | } 1050 | 1051 | /** 1052 | * Handle the first 4 bytes of a message, to determine the length of bytes that must be 1053 | * waited for in order to have the whole message. 1054 | * @param {Uint8Array} buffer 1055 | */ 1056 | _onMessageLength (buffer) { 1057 | const length = getUint32(buffer) 1058 | if (length > 0) { 1059 | this._parse(length, this._onMessage) 1060 | } else { 1061 | this._onKeepAlive() 1062 | this._parse(4, this._onMessageLength) 1063 | } 1064 | } 1065 | 1066 | /** 1067 | * Handle a message from the remote peer. 1068 | * @param {Uint8Array} buffer 1069 | */ 1070 | _onMessage (buffer) { 1071 | this._parse(4, this._onMessageLength) 1072 | switch (buffer[0]) { 1073 | case 0: 1074 | return this._onChoke() 1075 | case 1: 1076 | return this._onUnchoke() 1077 | case 2: 1078 | return this._onInterested() 1079 | case 3: 1080 | return this._onUninterested() 1081 | case 4: 1082 | return this._onHave(getUint32(buffer, 1)) 1083 | case 5: 1084 | return this._onBitField(buffer.subarray(1)) 1085 | case 6: 1086 | return this._onRequest( 1087 | getUint32(buffer, 1), 1088 | getUint32(buffer, 5), 1089 | getUint32(buffer, 9) 1090 | ) 1091 | case 7: 1092 | return this._onPiece( 1093 | getUint32(buffer, 1), 1094 | getUint32(buffer, 5), 1095 | buffer.subarray(9) 1096 | ) 1097 | case 8: 1098 | return this._onCancel( 1099 | getUint32(buffer, 1), 1100 | getUint32(buffer, 5), 1101 | getUint32(buffer, 9) 1102 | ) 1103 | case 9: 1104 | return this._onPort((buffer[1] << 8) | buffer[2]) 1105 | case 0x0D: 1106 | return this._onSuggest(getUint32(buffer, 1)) 1107 | case 0x0E: 1108 | return this._onHaveAll() 1109 | case 0x0F: 1110 | return this._onHaveNone() 1111 | case 0x10: 1112 | return this._onReject( 1113 | getUint32(buffer, 1), 1114 | getUint32(buffer, 5), 1115 | getUint32(buffer, 9) 1116 | ) 1117 | case 0x11: 1118 | return this._onAllowedFast(getUint32(buffer, 1)) 1119 | case 20: 1120 | return this._onExtended(buffer[1], buffer.subarray(2)) 1121 | default: 1122 | this._debug('got unknown message') 1123 | return this.emit('unknownmessage', buffer) 1124 | } 1125 | } 1126 | 1127 | _determineHandshakeType () { 1128 | this._parse(1, pstrLenBuffer => { 1129 | const pstrlen = pstrLenBuffer[0] 1130 | if (pstrlen === 19) { 1131 | this._parse(pstrlen + 48, this._onHandshakeBuffer) 1132 | } else { 1133 | this._parsePe1(pstrLenBuffer) 1134 | } 1135 | }) 1136 | } 1137 | 1138 | _parsePe1 (pubKeyPrefix) { 1139 | this._parse(95, pubKeySuffix => { 1140 | this._onPe1(concat([pubKeyPrefix, pubKeySuffix])) 1141 | this._parsePe3() 1142 | }) 1143 | } 1144 | 1145 | _parsePe2 () { 1146 | this._parse(96, async pubKey => { 1147 | this._onPe2(pubKey) 1148 | if (!this._setGenerators) { 1149 | // Wait until generators have been set 1150 | await new Promise(resolve => this.once('_generators', resolve)) 1151 | } 1152 | this._parsePe4() 1153 | }) 1154 | } 1155 | 1156 | // Handles the unencrypted portion of step 4 1157 | async _parsePe3 () { 1158 | const hash1Buffer = await hash(hex2arr(this._utfToHex('req1') + this._sharedSecret)) 1159 | // synchronize on HASH('req1', S) 1160 | this._parseUntil(hash1Buffer, 512) 1161 | this._parse(20, async buffer => { 1162 | this._onPe3(buffer) 1163 | if (!this._setGenerators) { 1164 | // Wait until generators have been set 1165 | await new Promise(resolve => this.once('_generators', resolve)) 1166 | } 1167 | this._parsePe3Encrypted() 1168 | }) 1169 | } 1170 | 1171 | _parsePe3Encrypted () { 1172 | this._parse(14, buffer => { 1173 | const vcBuffer = this._decryptHandshake(buffer.slice(0, 8)) 1174 | const peerProvideBuffer = this._decryptHandshake(buffer.slice(8, 12)) 1175 | const padCLen = new DataView(this._decryptHandshake(buffer.slice(12, 14)).buffer).getUint16(0) 1176 | this._parse(padCLen, padCBuffer => { 1177 | padCBuffer = this._decryptHandshake(padCBuffer) 1178 | this._parse(2, iaLenBuf => { 1179 | const iaLen = new DataView(this._decryptHandshake(iaLenBuf).buffer).getUint16(0) 1180 | this._parse(iaLen, iaBuffer => { 1181 | iaBuffer = this._decryptHandshake(iaBuffer) 1182 | this._onPe3Encrypted(vcBuffer, peerProvideBuffer, padCBuffer, iaBuffer) 1183 | const pstrlen = iaLen ? iaBuffer[0] : null 1184 | const protocol = iaLen ? iaBuffer.slice(1, 20) : null 1185 | if (pstrlen === 19 && arr2text(protocol) === 'BitTorrent protocol') { 1186 | this._onHandshakeBuffer(iaBuffer.slice(1)) 1187 | } else { 1188 | this._parseHandshake() 1189 | } 1190 | }) 1191 | }) 1192 | }) 1193 | }) 1194 | } 1195 | 1196 | _parsePe4 () { 1197 | // synchronize on ENCRYPT(VC). 1198 | // since we encrypt using bitwise xor, decryption and encryption are the same operation. 1199 | // calling _decryptHandshake here advances the decrypt generator keystream forward 8 bytes 1200 | const vcBufferEncrypted = this._decryptHandshake(VC) 1201 | this._parseUntil(vcBufferEncrypted, 512) 1202 | this._parse(6, buffer => { 1203 | const peerSelectBuffer = this._decryptHandshake(buffer.slice(0, 4)) 1204 | const padDLen = new DataView(this._decryptHandshake(buffer.slice(4, 6)).buffer).getUint16(0) 1205 | this._parse(padDLen, padDBuf => { 1206 | this._decryptHandshake(padDBuf) 1207 | this._onPe4(peerSelectBuffer) 1208 | this._parseHandshake(null) 1209 | }) 1210 | }) 1211 | } 1212 | 1213 | /** 1214 | * Reads the handshake as specified by the bittorrent wire protocol. 1215 | */ 1216 | _parseHandshake () { 1217 | this._parse(1, buffer => { 1218 | const pstrlen = buffer[0] 1219 | if (pstrlen !== 19) { 1220 | this._debug('Error: wire not speaking BitTorrent protocol (%s)', pstrlen.toString()) 1221 | this.end() 1222 | return 1223 | } 1224 | this._parse(pstrlen + 48, this._onHandshakeBuffer) 1225 | }) 1226 | } 1227 | 1228 | _onHandshakeBuffer (handshake) { 1229 | const protocol = handshake.slice(0, 19) 1230 | if (arr2text(protocol) !== 'BitTorrent protocol') { 1231 | this._debug('Error: wire not speaking BitTorrent protocol (%s)', arr2text(protocol)) 1232 | this.end() 1233 | return 1234 | } 1235 | handshake = handshake.slice(19) 1236 | this._onHandshake(handshake.slice(8, 28), handshake.slice(28, 48), { 1237 | dht: !!(handshake[7] & 0x01), // see bep_0005 1238 | fast: !!(handshake[7] & 0x04), // see bep_0006 1239 | extended: !!(handshake[5] & 0x10) // see bep_0010 1240 | }) 1241 | this._parse(4, this._onMessageLength) 1242 | } 1243 | 1244 | _onFinish () { 1245 | this._finished = true 1246 | 1247 | this.push(null) // stream cannot be half open, so signal the end of it 1248 | while (this.read()) { 1249 | // body intentionally empty 1250 | // consume and discard the rest of the stream data 1251 | } 1252 | 1253 | clearInterval(this._keepAliveInterval) 1254 | this._parse(Number.MAX_VALUE, () => {}) 1255 | while (this.peerRequests.length) { 1256 | this.peerRequests.pop() 1257 | } 1258 | while (this.requests.length) { 1259 | this._callback(this.requests.pop(), new Error('wire was closed'), null) 1260 | } 1261 | } 1262 | 1263 | _debug (...args) { 1264 | args[0] = `[${this._debugId}] ${args[0]}` 1265 | debug(...args) 1266 | } 1267 | 1268 | _pull (requests, piece, offset, length) { 1269 | for (let i = 0; i < requests.length; i++) { 1270 | const req = requests[i] 1271 | if (req.piece === piece && req.offset === offset && req.length === length) { 1272 | arrayRemove(requests, i) 1273 | return req 1274 | } 1275 | } 1276 | return null 1277 | } 1278 | 1279 | _encryptHandshake (buf) { 1280 | const crypt = new Uint8Array(buf) 1281 | if (!this._encryptGenerator) { 1282 | this._debug('Warning: Encrypting without any generator') 1283 | return crypt 1284 | } 1285 | 1286 | for (let i = 0; i < buf.length; i++) { 1287 | const keystream = this._encryptGenerator.randomByte() 1288 | crypt[i] = crypt[i] ^ keystream 1289 | } 1290 | 1291 | return crypt 1292 | } 1293 | 1294 | _encrypt (buf) { 1295 | const crypt = new Uint8Array(buf) 1296 | 1297 | if (!this._encryptGenerator || this._encryptionMethod !== 2) { 1298 | return crypt 1299 | } 1300 | for (let i = 0; i < buf.length; i++) { 1301 | const keystream = this._encryptGenerator.randomByte() 1302 | crypt[i] = crypt[i] ^ keystream 1303 | } 1304 | 1305 | return crypt 1306 | } 1307 | 1308 | _decryptHandshake (buf) { 1309 | const decrypt = new Uint8Array(buf) 1310 | 1311 | if (!this._decryptGenerator) { 1312 | this._debug('Warning: Decrypting without any generator') 1313 | return decrypt 1314 | } 1315 | for (let i = 0; i < buf.length; i++) { 1316 | const keystream = this._decryptGenerator.randomByte() 1317 | decrypt[i] = decrypt[i] ^ keystream 1318 | } 1319 | 1320 | return decrypt 1321 | } 1322 | 1323 | _decrypt (buf) { 1324 | const decrypt = new Uint8Array(buf) 1325 | 1326 | if (!this._decryptGenerator || this._encryptionMethod !== 2) { 1327 | return decrypt 1328 | } 1329 | for (let i = 0; i < buf.length; i++) { 1330 | const keystream = this._decryptGenerator.randomByte() 1331 | decrypt[i] = decrypt[i] ^ keystream 1332 | } 1333 | 1334 | return decrypt 1335 | } 1336 | 1337 | _utfToHex (str) { 1338 | return arr2hex(text2arr(str)) 1339 | } 1340 | } 1341 | 1342 | export default Wire 1343 | --------------------------------------------------------------------------------