├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test └── basic.js /.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: ${{ matrix.node }} 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .github/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.5](https://github.com/webtorrent/lt_donthave/compare/v2.0.4...v2.0.5) (2025-05-15) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependency debug to v4.4.1 ([#53](https://github.com/webtorrent/lt_donthave/issues/53)) ([b33ab75](https://github.com/webtorrent/lt_donthave/commit/b33ab7579a491961bff57eacb8154e8343cd1cab)) 7 | 8 | ## [2.0.4](https://github.com/webtorrent/lt_donthave/compare/v2.0.3...v2.0.4) (2024-12-07) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency debug to v4.4.0 ([#51](https://github.com/webtorrent/lt_donthave/issues/51)) ([9a0d53d](https://github.com/webtorrent/lt_donthave/commit/9a0d53d99bfdbc71ae163a561c1365538df649eb)) 14 | 15 | ## [2.0.3](https://github.com/webtorrent/lt_donthave/compare/v2.0.2...v2.0.3) (2024-09-07) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update dependency debug to v4.3.7 ([#47](https://github.com/webtorrent/lt_donthave/issues/47)) ([a8a345c](https://github.com/webtorrent/lt_donthave/commit/a8a345c8ab2ab5044e542f4d30327f16ab467d11)) 21 | 22 | ## [2.0.2](https://github.com/webtorrent/lt_donthave/compare/v2.0.1...v2.0.2) (2024-07-28) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency debug to v4.3.6 ([#46](https://github.com/webtorrent/lt_donthave/issues/46)) ([9a8ced4](https://github.com/webtorrent/lt_donthave/commit/9a8ced4f8d130439b5e77bf6a139f89168d73868)) 28 | 29 | ## [2.0.1](https://github.com/webtorrent/lt_donthave/compare/v2.0.0...v2.0.1) (2024-06-01) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** update dependency debug to v4.3.5 ([#39](https://github.com/webtorrent/lt_donthave/issues/39)) ([b82facf](https://github.com/webtorrent/lt_donthave/commit/b82facf39a387e38b364ff49cba63ba44d88b38c)) 35 | 36 | # [2.0.0](https://github.com/webtorrent/lt_donthave/compare/v1.0.1...v2.0.0) (2023-05-31) 37 | 38 | 39 | ### Features 40 | 41 | * esm ([#15](https://github.com/webtorrent/lt_donthave/issues/15)) ([2f29eb5](https://github.com/webtorrent/lt_donthave/commit/2f29eb5e9c0d7844df0c928626437cfe3909b7fa)) 42 | 43 | 44 | ### BREAKING CHANGES 45 | 46 | * ESM only 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) John Hiesey 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lt_donthave [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [ci-image]: https://github.com/webtorrent/lt_donthave/actions/workflows/ci.yml/badge.svg?branch=master 4 | [ci-url]: https://github.com/webtorrent/lt_donthave/actions 5 | [npm-image]: https://img.shields.io/npm/v/lt_donthave.svg 6 | [npm-url]: https://npmjs.org/package/lt_donthave 7 | [downloads-image]: https://img.shields.io/npm/dm/lt_donthave.svg 8 | [downloads-url]: https://npmjs.org/package/lt_donthave 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | ### The BitTorrent lt_donthave extension (BEP 54) 13 | 14 | JavaScript implementation of the [The BitTorrent lt_donthave extension (BEP 54)](https://www.bittorrent.org/beps/bep_0054.html). Use with [bittorrent-protocol](https://www.npmjs.com/package/bittorrent-protocol). 15 | 16 | The purpose of this extension is to allow peers to indicate that they no longer have a piece. It provides a single `donthave` message that means the opposite of the standard `have` message. In addition, when a client receives `donthave`, it knows that all requests for the matching piece have failed. 17 | 18 | Works in the browser with [browserify](http://browserify.org/)! This module is used by [WebTorrent](http://webtorrent.io). 19 | 20 | ### install 21 | 22 | ``` 23 | npm install lt_donthave 24 | ``` 25 | 26 | ### usage 27 | 28 | This package should be used with [bittorrent-protocol](https://www.npmjs.com/package/bittorrent-protocol), which supports a plugin-like system for extending the protocol with additional functionality. 29 | 30 | Say you're already using `bittorrent-protocol`. Your code might look something like this: 31 | 32 | ```js 33 | import BitField from 'bitfield' 34 | import Protocol from 'bittorrent-protocol' 35 | import net from 'net' 36 | 37 | net.createServer(socket => { 38 | var wire = new Protocol() 39 | socket.pipe(wire).pipe(socket) 40 | 41 | // handle handshake 42 | wire.on('handshake', (infoHash, peerId) => { 43 | wire.handshake(Buffer.from('my info hash'), Buffer.from('my peer id')) 44 | 45 | // advertise that we have all 10 pieces of the torrent 46 | const bitfield = new BitField(10) 47 | for (let i = 0; i <= 10; i++) { 48 | bitfield.set(i, true) 49 | } 50 | wire.bitfield(bitfield) 51 | }) 52 | 53 | }).listen(6881) 54 | ``` 55 | 56 | To add support for BEP 54, simply modify your code like this: 57 | 58 | ```js 59 | import BitField from 'bitfield' 60 | import Protocol from 'bittorrent-protocol' 61 | import net from 'net' 62 | import lt_donthave from 'lt_donthave' 63 | 64 | net.createServer(socket => { 65 | const wire = new Protocol() 66 | socket.pipe(wire).pipe(socket) 67 | 68 | // initialize the extension 69 | wire.use(lt_donthave()) 70 | 71 | // all `lt_donthave` functionality can now be accessed at wire.lt_donthave 72 | 73 | wire.on('request', (pieceIndex, offset, length, cb) => { 74 | // whoops, turns out we don't have any pieces after all 75 | wire.lt_donthave.donthave(pieceIndex) 76 | cb(new Error('not found')) 77 | }) 78 | 79 | // 'donthave' event will fire when the remote peer indicates it no longer has a piece 80 | wire.lt_donthave.on('donthave', index => { 81 | // remote peer no longer has piece `index` 82 | }) 83 | 84 | // handle handshake 85 | wire.on('handshake', (infoHash, peerId) => { 86 | wire.handshake(Buffer.from('my info hash'), Buffer.from('my peer id')) 87 | 88 | // advertise that we have all 10 pieces of the torrent 89 | const bitfield = new BitField(10) 90 | for (let i = 0; i <= 10; i++) { 91 | bitfield.set(i, true) 92 | } 93 | wire.bitfield(bitfield) 94 | }) 95 | 96 | }).listen(6881) 97 | ``` 98 | 99 | ### api 100 | 101 | #### `lt_donthave()` 102 | 103 | Initialize the extension. 104 | 105 | ```js 106 | wire.use(lt_donthave()) 107 | ``` 108 | 109 | #### `lt_donthave.donthave(index)` 110 | 111 | Tell the remote peer that this peer no longer has the piece with the specified `index`. 112 | 113 | Opposite of `wire.have`. 114 | 115 | #### `lt_donthave.on('donthave', index => {})` 116 | 117 | Fired when the remote peer no longer has the piece with the specified `index`. 118 | 119 | Opposite of `wire.on('have', index => ())` 120 | 121 | After this is fired, all outstanding requests to the remote peer for that piece will automatically fail. 122 | 123 | ### license 124 | 125 | MIT. Copyright (c) John Hiesey and [WebTorrent, LLC](https://webtorrent.io). 126 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! lt_donthave. MIT License. WebTorrent LLC */ 2 | import arrayRemove from 'unordered-array-remove' 3 | import { EventEmitter } from 'events' 4 | import debugFactory from 'debug' 5 | 6 | const debug = debugFactory('lt_donthave') 7 | 8 | export default () => { 9 | class ltDontHave extends EventEmitter { 10 | constructor (wire) { 11 | super() 12 | 13 | this._peerSupports = false 14 | this._wire = wire 15 | } 16 | 17 | onExtendedHandshake () { 18 | this._peerSupports = true 19 | } 20 | 21 | onMessage (buf) { 22 | let index 23 | try { 24 | const view = new DataView(buf.buffer) 25 | index = view.getUint32(0) 26 | } catch (err) { 27 | // drop invalid messages 28 | return 29 | } 30 | 31 | if (!this._wire.peerPieces.get(index)) return 32 | debug('got donthave %d', index) 33 | this._wire.peerPieces.set(index, false) 34 | 35 | this.emit('donthave', index) 36 | this._failRequests(index) 37 | } 38 | 39 | donthave (index) { 40 | if (!this._peerSupports) return 41 | 42 | debug('donthave %d', index) 43 | const buf = new Uint8Array(4) 44 | const view = new DataView(buf.buffer) 45 | view.setUint32(0, index) 46 | 47 | this._wire.extended('lt_donthave', buf) 48 | } 49 | 50 | _failRequests (index) { 51 | const requests = this._wire.requests 52 | for (let i = 0; i < requests.length; i++) { 53 | const req = requests[i] 54 | if (req.piece === index) { 55 | arrayRemove(requests, i) 56 | i -= 1 // Check the new value at the same slot 57 | this._wire._callback(req, new Error('peer sent donthave'), null) 58 | } 59 | } 60 | } 61 | } 62 | 63 | // Name of the bittorrent-protocol extension 64 | ltDontHave.prototype.name = 'lt_donthave' 65 | 66 | return ltDontHave 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lt_donthave", 3 | "description": "The BitTorrent lt_donthave extension (BEP 54)", 4 | "version": "2.0.5", 5 | "author": { 6 | "name": "WebTorrent LLC", 7 | "email": "feross@webtorrent.io", 8 | "url": "https://webtorrent.io" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/webtorrent/lt_donthave/issues" 12 | }, 13 | "type": "module", 14 | "dependencies": { 15 | "debug": "^4.2.0", 16 | "unordered-array-remove": "^1.0.2" 17 | }, 18 | "devDependencies": { 19 | "@webtorrent/semantic-release-config": "^1.0.9", 20 | "bittorrent-protocol": "4.1.16", 21 | "brfs": "2.0.2", 22 | "standard": "*", 23 | "tape": "5.9.0", 24 | "webtorrent-fixtures": "2.0.2" 25 | }, 26 | "keywords": [ 27 | "bep", 28 | "bep 54", 29 | "bep_0054", 30 | "bittorrent", 31 | "p2p", 32 | "torrent", 33 | "lt_donthave" 34 | ], 35 | "license": "MIT", 36 | "engines": { 37 | "node": ">=12.20.0" 38 | }, 39 | "exports": { 40 | "import": "./index.js" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git://github.com/webtorrent/lt_donthave.git" 45 | }, 46 | "scripts": { 47 | "test": "standard && tape test/*.js" 48 | }, 49 | "funding": [ 50 | { 51 | "type": "github", 52 | "url": "https://github.com/sponsors/feross" 53 | }, 54 | { 55 | "type": "patreon", 56 | "url": "https://www.patreon.com/feross" 57 | }, 58 | { 59 | "type": "consulting", 60 | "url": "https://feross.org/support" 61 | } 62 | ], 63 | "renovate": { 64 | "extends": [ 65 | "github>webtorrent/renovate-config" 66 | ] 67 | }, 68 | "release": { 69 | "extends": "@webtorrent/semantic-release-config" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import Protocol from 'bittorrent-protocol' 3 | import test from 'tape' 4 | import ltDontHave from '../index.js' 5 | 6 | const { leaves } = fixtures 7 | 8 | const id1 = Buffer.from('01234567890123456789') 9 | const id2 = Buffer.from('12345678901234567890') 10 | 11 | test('wire.use(ltDontHave())', t => { 12 | const wire = new Protocol() 13 | wire.pipe(wire) 14 | 15 | wire.use(ltDontHave()) 16 | 17 | t.ok(wire.lt_donthave) 18 | t.ok(wire.lt_donthave.donthave) 19 | t.end() 20 | }) 21 | 22 | test('donthave sent over the wire', t => { 23 | t.plan(3) 24 | 25 | const wire1 = new Protocol() 26 | wire1.peerPieces.set(30, true) 27 | const wire2 = new Protocol() 28 | wire1.pipe(wire2).pipe(wire1) 29 | 30 | wire1.use(ltDontHave()) 31 | wire2.use(ltDontHave()) 32 | 33 | wire2.on('handshake', (infoHash, peerId, extensions) => { 34 | wire2.handshake(leaves.parsedTorrent.infoHash, id2) 35 | }) 36 | 37 | wire2.on('extended', ext => { 38 | if (ext === 'handshake') { 39 | t.pass('wire2 got extended handshake') 40 | wire2.lt_donthave.donthave(30) 41 | } 42 | }) 43 | 44 | wire1.lt_donthave.on('donthave', (index) => { 45 | t.equal(index, 30) 46 | t.notOk(wire1.peerPieces.get(30), 'piece 30 cleared in bitfield') 47 | }) 48 | 49 | wire1.handshake(leaves.parsedTorrent.infoHash, id1) 50 | }) 51 | 52 | test('donthave ignored for pieces the peer doesn\'t already have', t => { 53 | t.plan(1) 54 | 55 | const wire1 = new Protocol() 56 | const wire2 = new Protocol() 57 | wire1.pipe(wire2).pipe(wire1) 58 | 59 | wire1.use(ltDontHave()) 60 | wire2.use(ltDontHave()) 61 | 62 | wire2.on('handshake', (infoHash, peerId, extensions) => { 63 | wire2.handshake(leaves.parsedTorrent.infoHash, id2) 64 | }) 65 | 66 | wire2.on('extended', ext => { 67 | if (ext === 'handshake') { 68 | t.pass('wire2 got extended handshake') 69 | wire2.lt_donthave.donthave(30) 70 | } 71 | }) 72 | 73 | wire1.lt_donthave.on('donthave', (index) => { 74 | t.fail('should have been filtered out') 75 | }) 76 | 77 | wire1.handshake(leaves.parsedTorrent.infoHash, id1) 78 | }) 79 | 80 | test('donthave works bidirectionally', t => { 81 | t.plan(6) 82 | 83 | const wire1 = new Protocol() 84 | wire1.peerPieces.set(30, true) 85 | const wire2 = new Protocol() 86 | wire2.peerPieces.set(20, true) 87 | wire1.pipe(wire2).pipe(wire1) 88 | 89 | wire1.use(ltDontHave()) 90 | wire2.use(ltDontHave()) 91 | 92 | wire2.on('handshake', (infoHash, peerId, extensions) => { 93 | wire2.handshake(leaves.parsedTorrent.infoHash, id2) 94 | }) 95 | 96 | wire1.on('extended', ext => { 97 | if (ext === 'handshake') { 98 | t.pass('wire1 got extended handshake') 99 | wire1.lt_donthave.donthave(20) 100 | } 101 | }) 102 | 103 | wire2.on('extended', ext => { 104 | if (ext === 'handshake') { 105 | t.pass('wire2 got extended handshake') 106 | wire2.lt_donthave.donthave(30) 107 | } 108 | }) 109 | 110 | wire1.lt_donthave.on('donthave', (index) => { 111 | t.equal(index, 30) 112 | t.notOk(wire1.peerPieces.get(30), 'piece 30 cleared in bitfield') 113 | }) 114 | 115 | wire2.lt_donthave.on('donthave', (index) => { 116 | t.equal(index, 20) 117 | t.notOk(wire2.peerPieces.get(20), 'piece 20 cleared in bitfield') 118 | }) 119 | 120 | wire1.handshake(leaves.parsedTorrent.infoHash, id1) 121 | }) 122 | 123 | test('requests fail when matching donthave arrives', t => { 124 | t.plan(5) 125 | 126 | const wire1 = new Protocol() 127 | const wire2 = new Protocol() 128 | wire1.peerPieces.set(20, true) 129 | wire1.peerPieces.set(30, true) 130 | wire1.pipe(wire2).pipe(wire1) 131 | 132 | wire1.use(ltDontHave()) 133 | wire2.use(ltDontHave()) 134 | 135 | wire2.on('handshake', (infoHash, peerId, extensions) => { 136 | wire2.handshake(leaves.parsedTorrent.infoHash, id2) 137 | wire2.unchoke() 138 | }) 139 | 140 | wire1.on('unchoke', () => { 141 | wire1.request(20, 0, 16384, (err) => { 142 | t.error(err, 'piece 20 succeeded as expected') 143 | }) 144 | wire1.request(30, 0, 16384, (err) => { 145 | t.ok(err instanceof Error, 'piece 30 failed as expected') 146 | t.notOk(wire1.peerPieces.get(30), 'piece 30 cleared in bitfield') 147 | }) 148 | }) 149 | 150 | wire2.on('request', (pieceIndex, chunkOffset, chunkLength, onChunk) => { 151 | if (pieceIndex === 20) { 152 | t.pass('got request for piece 20') 153 | onChunk(null, Buffer.alloc(16384)) 154 | } else if (pieceIndex === 30) { 155 | t.pass('got request for piece 30') 156 | wire2.lt_donthave.donthave(30) 157 | // intentionally not calling `onChunk` 158 | } else { 159 | t.fail('got request for unexpected piece') 160 | } 161 | }) 162 | 163 | wire1.handshake(leaves.parsedTorrent.infoHash, id1) 164 | }) 165 | --------------------------------------------------------------------------------