diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de46e32..290ad02 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,5 +4,5 @@ updates: directory: "/" schedule: interval: daily - time: "11:00" + time: "10:00" open-pull-requests-limit: 10 diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..13da9c1 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,50 @@ +# Automatically merge pull requests opened by web3-bot, as soon as (and only if) all tests pass. +# This reduces the friction associated with updating with our workflows. + +on: [ pull_request ] +name: Automerge + +jobs: + automerge-check: + if: github.event.pull_request.user.login == 'web3-bot' + runs-on: ubuntu-latest + outputs: + status: ${{ steps.should-automerge.outputs.status }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Check if we should automerge + id: should-automerge + run: | + for commit in $(git rev-list --first-parent origin/${{ github.event.pull_request.base.ref }}..${{ github.event.pull_request.head.sha }}); do + committer=$(git show --format=$'%ce' -s $commit) + echo "Committer: $committer" + if [[ "$committer" != "web3-bot@users.noreply.github.com" ]]; then + echo "Commit $commit wasn't committed by web3-bot, but by $committer." + echo "::set-output name=status::false" + exit + fi + done + echo "::set-output name=status::true" + automerge: + needs: automerge-check + runs-on: ubuntu-latest + # The check for the user is redundant here, as this job depends on the automerge-check job, + # but it prevents this job from spinning up, just to be skipped shortly after. + if: github.event.pull_request.user.login == 'web3-bot' && needs.automerge-check.outputs.status == 'true' + steps: + - name: Wait on tests + uses: lewagon/wait-on-check-action@bafe56a6863672c681c3cf671f5e10b20abf2eaa # v0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + running-workflow-name: 'automerge' # the name of this job + - name: Merge PR + uses: pascalgn/automerge-action@741c311a47881be9625932b0a0de1b0937aab1ae # v0.13.1 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MERGE_LABELS: "" + MERGE_METHOD: "squash" + MERGE_DELETE_BRANCH: true diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml new file mode 100644 index 0000000..8630dc5 --- /dev/null +++ b/.github/workflows/js-test-and-release.yml @@ -0,0 +1,152 @@ +name: test & maybe release +on: + push: + branches: + - master # with #262 - ${{{ github.default_branch }}} + pull_request: + branches: + - master # with #262 - ${{{ github.default_branch }}} + +jobs: + + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present lint + - run: npm run --if-present dep-check + + test-node: + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [16] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:node + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: node + + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: chrome + + test-chrome-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome-webworker + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: chrome-webworker + + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: firefox + + test-firefox-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox-webworker + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: firefox-webworker + + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-main + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: electron-main + + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-renderer + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: electron-renderer + + release: + needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main, test-electron-renderer] + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' # with #262 - 'refs/heads/${{{ github.default_branch }}}' + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - uses: ipfs/aegir/actions/docker-login@master + with: + docker-token: ${{ secrets.DOCKER_TOKEN }} + docker-username: ${{ secrets.DOCKER_USERNAME }} + - run: npm run --if-present release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 5489d89..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: ci -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - run: npm install - - run: npx aegir lint - - run: npx aegir dep-check - test-node: - needs: check - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [windows-latest, ubuntu-latest, macos-latest] - node: [16] - fail-fast: true - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - run: npm install - - run: npx aegir test -t node --bail --cov - - uses: codecov/codecov-action@v1 diff --git a/LICENSE b/LICENSE index 59a33ba..20ce483 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,4 @@ -The MIT License (MIT) - -Copyright (c) 2015 David Dias - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +This project is dual licensed under MIT and Apache-2.0. +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..14478a3 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..72dc60d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index ac5614a..1e8188f 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,16 @@ -libp2p-mdns JavaScript implementation -===================================== +# libp2p-mdns JavaScript implementation [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-mdns.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-mdns) -[![](https://img.shields.io/travis/libp2p/js-libp2p-mdns.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-mdns) +[![Build Status](https://github.com/libp2p/js-libp2p-mdns/actions/workflows/js-test-and-release.yml/badge.svg?branch=main)](https://github.com/libp2p/js-libp2p-mdns/actions/workflows/js-test-and-release.yml) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-mdns.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-mdns) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) > JavaScript libp2p MulticastDNS discovery implementation -## Lead Maintainer - -[Jacob Heun](https://github.com/jacobheun) - ## Usage ```JavaScript @@ -24,7 +19,7 @@ const MDNS = require('libp2p-mdns') const mdns = new MDNS(options) mdns.on('peer', (peerData) => { - console.log('Found a peer in the local network', peerData.id.toB58String(), peerData.multiaddrs) + console.log('Found a peer in the local network', peerData.id.toString(base58btc), peerData.multiaddrs) }) // Broadcast for 20 seconds @@ -83,3 +78,21 @@ When a query is detected, each IPFS node sends an answer about itself ttl: 120, data: '::1' } ] ``` + +## Contribute + +The libp2p implementation in JavaScript is a work in progress. As such, there are a few things you can do right now to help out: + + - Go through the modules and **check out existing issues**. This is especially useful for modules in active development. Some knowledge of IPFS/libp2p may be required, as well as the infrastructure behind it - for instance, you may need to read up on p2p and more complex operations like muxing to be able to help technically. + - **Perform code reviews**. More eyes will help a) speed the project along b) ensure quality and c) reduce possible future bugs. + +## License + +Licensed under either of + + * Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0) + * MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT) + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/package.json b/package.json index d7c7498..698995d 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,148 @@ { - "name": "libp2p-mdns", + "name": "@libp2p/mdns", "version": "0.18.0", "description": "Node.js libp2p mDNS discovery implementation for peer discovery", - "leadMaintainer": "Jacob Heun ", - "main": "src/index.js", - "files": [ - "dist", - "src" - ], - "scripts": { - "lint": "aegir lint", - "coverage": "nyc --reporter=lcov --reporter=text npm run test:node", - "test": "aegir test -t node", - "test:node": "aegir test -t node", - "release": "aegir release -t node --no-build", - "release-minor": "aegir release --type minor -t node --no-build", - "release-major": "aegir release --type major -t node --no-build", - "coverage-publish": "aegir coverage publish" - }, + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-mdns#readme", "repository": { "type": "git", - "url": "https://github.com/libp2p/js-libp2p-mdns.git" + "url": "git+https://github.com/libp2p/js-libp2p-mdns.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-mdns/issues" }, "keywords": [ "IPFS" ], - "license": "MIT", - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-mdns/issues" + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" }, - "homepage": "https://github.com/libp2p/js-libp2p-mdns", - "devDependencies": { - "aegir": "^36.0.2", - "delay": "^5.0.0", - "libp2p-interfaces": "^2.0.1", - "libp2p-interfaces-compliance-tests": "^2.0.1", - "p-defer": "^3.0.0", - "p-wait-for": "^3.1.0" + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "master" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "chore", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Trivial Changes" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "lint": "aegir lint", + "dep-check": "aegir dep-check dist/src/**/*.js dist/test/**/*.js", + "build": "tsc", + "pretest": "npm run build", + "test": "aegir test -f ./dist/test", + "test:node": "npm run test -- -t node --cov", + "test:electron-main": "npm run test -- -t electron-main", + "release": "semantic-release" }, "dependencies": { - "debug": "^4.3.1", - "multiaddr": "^10.0.0", + "@libp2p/logger": "^1.0.2", + "@libp2p/peer-id": "^1.1.1", + "@multiformats/multiaddr": "^10.0.0", "multicast-dns": "^7.2.0", - "peer-id": "^0.16.0" - }, - "contributors": [ - "David Dias ", - "Jacob Heun ", - "Nuno Nogueira ", - "Vasco Santos ", - "Alex Potsides ", - "Friedel Ziegelmayer ", - "Richard Schneider ", - "Alan Shaw ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ ", - "Richard Littauer ", - "Hubertus Hohl ", - "dirkmc ", - "emersion " - ] + "multiformats": "^9.6.3" + }, + "devDependencies": { + "@libp2p/interface-compliance-tests": "^1.1.3", + "@libp2p/interfaces": "^1.3.3", + "@libp2p/peer-id-factory": "^1.0.4", + "@types/multicast-dns": "^7.2.1", + "aegir": "^36.1.0", + "delay": "^5.0.0", + "p-defer": "^4.0.0", + "p-wait-for": "^4.1.0" + } } diff --git a/src/compat/constants.js b/src/compat/constants.js deleted file mode 100644 index df83a3f..0000000 --- a/src/compat/constants.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' - -exports.SERVICE_TAG = '_ipfs-discovery._udp' -exports.SERVICE_TAG_LOCAL = `${exports.SERVICE_TAG}.local` -exports.MULTICAST_IP = '224.0.0.251' -exports.MULTICAST_PORT = 5353 diff --git a/src/compat/constants.ts b/src/compat/constants.ts new file mode 100644 index 0000000..e3cc76d --- /dev/null +++ b/src/compat/constants.ts @@ -0,0 +1,4 @@ +export const SERVICE_TAG = '_ipfs-discovery._udp' +export const SERVICE_TAG_LOCAL = `${SERVICE_TAG}.local` +export const MULTICAST_IP = '224.0.0.251' +export const MULTICAST_PORT = 5353 diff --git a/src/compat/index.js b/src/compat/index.js deleted file mode 100644 index 4fe2bd4..0000000 --- a/src/compat/index.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' - -// Compatibility with Go libp2p MDNS - -const EE = require('events') -const Responder = require('./responder') -const Querier = require('./querier') - -class GoMulticastDNS extends EE { - constructor ({ peerId, multiaddrs, queryPeriod, queryInterval }) { - super() - this._started = false - this._peerId = peerId - this._multiaddrs = multiaddrs - this._queryPeriod = queryPeriod - this._queryInterval = queryInterval - this._onPeer = this._onPeer.bind(this) - } - - async start () { - if (this._started) { - return - } - - this._started = true - this._responder = new Responder({ - peerId: this._peerId, - multiaddrs: this._multiaddrs - }) - this._querier = new Querier({ - peerId: this._peerId, - queryInterval: this._queryInterval, - queryPeriod: this._queryPeriod - }) - - this._querier.on('peer', this._onPeer) - - await Promise.all([ - this._responder.start(), - this._querier.start() - ]) - } - - _onPeer (peerData) { - this.emit('peer', peerData) - } - - stop () { - if (!this._started) return - - const responder = this._responder - const querier = this._querier - - this._started = false - this._responder = null - this._querier = null - - querier.removeListener('peer', this._onPeer) - - return Promise.all([ - responder.stop(), - querier.stop() - ]) - } -} - -module.exports = GoMulticastDNS diff --git a/src/compat/index.ts b/src/compat/index.ts new file mode 100644 index 0000000..9e23c6d --- /dev/null +++ b/src/compat/index.ts @@ -0,0 +1,64 @@ +// Compatibility with Go libp2p MDNS +import { EventEmitter, CustomEvent } from '@libp2p/interfaces' +import { Responder } from './responder.js' +import { Querier } from './querier.js' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interfaces/peer-discovery' + +export class GoMulticastDNS extends EventEmitter implements PeerDiscovery { + private _started: boolean + private readonly _responder: Responder + private readonly _querier: Querier + + constructor (options: { peerId: PeerId, multiaddrs: Multiaddr[], queryPeriod?: number, queryInterval?: number }) { + super() + const { peerId, multiaddrs, queryPeriod, queryInterval } = options + + this._started = false + + this._responder = new Responder({ + peerId, + multiaddrs + }) + this._querier = new Querier({ + peerId, + queryInterval, + queryPeriod + }) + + this._querier.addEventListener('peer', (evt) => { + this.dispatchEvent(new CustomEvent('peer', { detail: evt.detail })) + }) + } + + isStarted () { + return this._started + } + + async start () { + if (this.isStarted()) { + return + } + + this._started = true + + await Promise.all([ + this._responder.start(), + this._querier.start() + ]) + } + + async stop () { + if (!this.isStarted()) { + return + } + + this._started = false + + await Promise.all([ + this._responder.stop(), + this._querier.stop() + ]) + } +} diff --git a/src/compat/querier.js b/src/compat/querier.js deleted file mode 100644 index da1ab3c..0000000 --- a/src/compat/querier.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict' - -const EE = require('events') -const MDNS = require('multicast-dns') -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') -const debug = require('debug') -const log = debug('libp2p:mdns:compat:querier') -log.error = debug('libp2p:mdns:compat:querier:error') -const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('./constants') - -class Querier extends EE { - constructor ({ peerId, queryInterval = 60000, queryPeriod }) { - super() - - if (!peerId) { - throw new Error('missing peerId parameter') - } - - this._peerIdStr = peerId.toB58String() - this._options = { - // Re-query in leu of network change detection (every 60s by default) - queryInterval: queryInterval, - // Time for which the MDNS server will stay alive waiting for responses - // Must be less than options.queryInterval! - queryPeriod: Math.min( - queryInterval, - queryPeriod == null ? 5000 : queryPeriod - ) - } - this._onResponse = this._onResponse.bind(this) - } - - start () { - this._handle = periodically(() => { - // Create a querier that queries multicast but gets responses unicast - const mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) - - mdns.on('response', this._onResponse) - - mdns.query({ - id: nextId(), // id > 0 for unicast response - questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] - }, null, { - address: MULTICAST_IP, - port: MULTICAST_PORT - }) - - return { - stop: () => { - mdns.removeListener('response', this._onResponse) - return new Promise(resolve => mdns.destroy(resolve)) - } - } - }, { - period: this._options.queryPeriod, - interval: this._options.queryInterval - }) - } - - _onResponse (event, info) { - const answers = event.answers || [] - const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) - - // Only deal with responses for our service tag - if (!ptrRecord) return - - log('got response', event, info) - - const txtRecord = answers.find(a => a.type === 'TXT') - if (!txtRecord) return log('missing TXT record in response') - - let peerIdStr - try { - peerIdStr = txtRecord.data[0].toString() - } catch (err) { - return log('failed to extract peer ID from TXT record data', txtRecord, err) - } - - if (this._peerIdStr === peerIdStr) { - return log('ignoring reply to myself') - } - - let peerId - try { - peerId = PeerId.createFromB58String(peerIdStr) - } catch (err) { - return log('failed to create peer ID from TXT record data', peerIdStr, err) - } - - const srvRecord = answers.find(a => a.type === 'SRV') - if (!srvRecord) return log('missing SRV record in response') - - log('peer found', peerIdStr) - - const { port } = srvRecord.data || {} - const protos = { A: 'ip4', AAAA: 'ip6' } - - const multiaddrs = answers - .filter(a => ['A', 'AAAA'].includes(a.type)) - .reduce((addrs, a) => { - const maStr = `/${protos[a.type]}/${a.data}/tcp/${port}` - try { - addrs.push(new Multiaddr(maStr)) - log(maStr) - } catch (err) { - log(`failed to create multiaddr from ${a.type} record data`, maStr, port, err) - } - return addrs - }, []) - - this.emit('peer', { - id: peerId, - multiaddrs - }) - } - - stop () { - return this._handle.stop() - } -} - -module.exports = Querier - -/** - * Run `fn` for a certain period of time, and then wait for an interval before - * running it again. `fn` must return an object with a stop function, which is - * called when the period expires. - * - * @param {Function} fn - function to run - * @param {Object} [options] - * @param {Object} [options.period] - Period in ms to run the function for - * @param {Object} [options.interval] - Interval in ms between runs - * @returns {Object} handle that can be used to stop execution - */ -function periodically (fn, options) { - let handle, timeoutId - let stopped = false - - const reRun = () => { - handle = fn() - timeoutId = setTimeout(async () => { - await handle.stop().catch(log) - if (!stopped) { - timeoutId = setTimeout(reRun, options.interval) - } - handle = null - }, options.period) - } - - reRun() - - return { - stop () { - stopped = true - clearTimeout(timeoutId) - if (handle) { - return handle.stop() - } - } - } -} - -const nextId = (() => { - let id = 0 - return () => { - id++ - if (id === Number.MAX_SAFE_INTEGER) id = 1 - return id - } -})() diff --git a/src/compat/querier.ts b/src/compat/querier.ts new file mode 100644 index 0000000..f443ebe --- /dev/null +++ b/src/compat/querier.ts @@ -0,0 +1,210 @@ +import { EventEmitter } from '@libp2p/interfaces' +import MDNS from 'multicast-dns' +import { Multiaddr } from '@multiformats/multiaddr' +import { PeerId } from '@libp2p/peer-id' +import { logger } from '@libp2p/logger' +import { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } from './constants.js' +import { base58btc } from 'multiformats/bases/base58' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interfaces/peer-discovery' +import type { ResponsePacket } from 'multicast-dns' +import type { RemoteInfo } from 'dgram' + +const log = logger('libp2p:mdns:compat:querier') + +export interface QuerierOptions { + peerId: PeerId + queryInterval?: number + queryPeriod?: number +} + +export interface Handle { + stop: () => Promise +} + +export class Querier extends EventEmitter implements PeerDiscovery { + private readonly _peerIdStr: string + private readonly _options: Required + private _handle?: Handle + + constructor (options: QuerierOptions) { + super() + + const { peerId, queryInterval, queryPeriod } = options + + if (peerId == null) { + throw new Error('missing peerId parameter') + } + + this._peerIdStr = peerId.toString(base58btc) + this._options = { + peerId, + + // Re-query in leu of network change detection (every 60s by default) + queryInterval: queryInterval ?? 60000, + // Time for which the MDNS server will stay alive waiting for responses + // Must be less than options.queryInterval! + queryPeriod: Math.min( + queryInterval ?? 60000, + queryPeriod ?? 5000 + ) + } + this._onResponse = this._onResponse.bind(this) + } + + isStarted () { + return Boolean(this._handle) + } + + start () { + this._handle = periodically(() => { + // Create a querier that queries multicast but gets responses unicast + const mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) + + mdns.on('response', this._onResponse) + + // @ts-expect-error @types/multicast-dns are wrong + mdns.query({ + id: nextId(), // id > 0 for unicast response + questions: [{ + name: SERVICE_TAG_LOCAL, + type: 'PTR', + class: 'IN' + }] + }, null, { + address: MULTICAST_IP, + port: MULTICAST_PORT + }) + + return { + stop: async () => { + mdns.removeListener('response', this._onResponse) + return await new Promise(resolve => mdns.destroy(resolve)) + } + } + }, { + period: this._options.queryPeriod, + interval: this._options.queryInterval + }) + } + + _onResponse (event: ResponsePacket, info: RemoteInfo) { + const answers = event.answers ?? [] + const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) + + // Only deal with responses for our service tag + if (ptrRecord == null) return + + log('got response', event, info) + + const txtRecord = answers.find(a => a.type === 'TXT') + if (txtRecord == null || txtRecord.type !== 'TXT') { + return log('missing TXT record in response') + } + + let peerIdStr + try { + peerIdStr = txtRecord.data[0].toString() + } catch (err) { + return log('failed to extract peer ID from TXT record data', txtRecord, err) + } + + if (this._peerIdStr === peerIdStr) { + return log('ignoring reply to myself') + } + + let peerId + try { + peerId = PeerId.fromString(peerIdStr) + } catch (err) { + return log('failed to create peer ID from TXT record data', peerIdStr, err) + } + + const srvRecord = answers.find(a => a.type === 'SRV') + if (srvRecord == null || srvRecord.type !== 'SRV') { + return log('missing SRV record in response') + } + + log('peer found', peerIdStr) + + const { port } = srvRecord.data ?? {} + const protos = { A: 'ip4', AAAA: 'ip6' } + + const multiaddrs = answers + .filter(a => ['A', 'AAAA'].includes(a.type)) + .reduce((addrs, a) => { + if (a.type !== 'A' && a.type !== 'AAAA') { + return addrs + } + + const maStr = `/${protos[a.type]}/${a.data}/tcp/${port}` + try { + addrs.push(new Multiaddr(maStr)) + log(maStr) + } catch (err) { + log(`failed to create multiaddr from ${a.type} record data`, maStr, port, err) + } + return addrs + }, []) + + this.dispatchEvent(new CustomEvent('peer', { + detail: { + id: peerId, + multiaddrs, + protcols: [] + } + })) + } + + async stop () { + if (this._handle != null) { + await this._handle.stop() + } + } +} + +/** + * Run `fn` for a certain period of time, and then wait for an interval before + * running it again. `fn` must return an object with a stop function, which is + * called when the period expires. + */ +function periodically (fn: () => Handle, options: { period: number, interval: number }) { + let handle: Handle | null + let timeoutId: NodeJS.Timer + let stopped = false + + const reRun = () => { + handle = fn() + timeoutId = setTimeout(() => { + if (handle != null) { + handle.stop().catch(log) + } + + if (!stopped) { + timeoutId = setTimeout(reRun, options.interval) + } + + handle = null + }, options.period) + } + + reRun() + + return { + async stop () { + stopped = true + clearTimeout(timeoutId) + if (handle != null) { + await handle.stop() + } + } + } +} + +const nextId = (() => { + let id = 0 + return () => { + id++ + if (id === Number.MAX_SAFE_INTEGER) id = 1 + return id + } +})() diff --git a/src/compat/responder.js b/src/compat/responder.js deleted file mode 100644 index 44f852d..0000000 --- a/src/compat/responder.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict' - -const OS = require('os') -const MDNS = require('multicast-dns') -const log = require('debug')('libp2p:mdns:compat:responder') -const { SERVICE_TAG_LOCAL } = require('./constants') - -class Responder { - constructor ({ peerId, multiaddrs }) { - if (!peerId) { - throw new Error('missing peerId parameter') - } - - this._peerId = peerId - this._peerIdStr = peerId.toB58String() - this._multiaddrs = multiaddrs - this._onQuery = this._onQuery.bind(this) - } - - start () { - this._mdns = MDNS() - this._mdns.on('query', this._onQuery) - } - - _onQuery (event, info) { - const addresses = this._multiaddrs.reduce((acc, addr) => { - if (addr.isThinWaistAddress()) { - acc.push(addr.toOptions()) - } - return acc - }, []) - - // Only announce TCP for now - if (!addresses.length) return - - const questions = event.questions || [] - - // Only respond to queries for our service tag - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return - - log('got query', event, info) - - const answers = [] - const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}` - - answers.push({ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }) - - // Only announce TCP multiaddrs for now - const port = addresses[0].port - - answers.push({ - name: peerServiceTagLocal, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port, - target: OS.hostname() - } - }) - - answers.push({ - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: [Buffer.from(this._peerIdStr)] - }) - - addresses.forEach((ma) => { - if ([4, 6].includes(ma.family)) { - answers.push({ - name: OS.hostname(), - type: ma.family === 4 ? 'A' : 'AAAA', - class: 'IN', - ttl: 120, - data: ma.host - }) - } - }) - - log('responding to query', answers) - this._mdns.respond(answers, info) - } - - stop () { - this._mdns.removeListener('query', this._onQuery) - return new Promise(resolve => this._mdns.destroy(resolve)) - } -} - -module.exports = Responder diff --git a/src/compat/responder.ts b/src/compat/responder.ts new file mode 100644 index 0000000..fefe145 --- /dev/null +++ b/src/compat/responder.ts @@ -0,0 +1,125 @@ +import OS from 'os' +import MDNS, { QueryPacket } from 'multicast-dns' +import { logger } from '@libp2p/logger' +import { SERVICE_TAG_LOCAL } from './constants.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { Multiaddr, MultiaddrObject } from '@multiformats/multiaddr' +import { base58btc } from 'multiformats/bases/base58' +import type { RemoteInfo } from 'dgram' +import type { Answer } from 'dns-packet' + +const log = logger('libp2p:mdns:compat:responder') + +export interface ResponderOptions { + peerId: PeerId + multiaddrs: Multiaddr[] +} + +export class Responder { + private readonly _peerIdStr: string + private readonly _multiaddrs: Multiaddr[] + private _mdns?: MDNS.MulticastDNS + + constructor (options: ResponderOptions) { + const { peerId, multiaddrs } = options + + if (peerId == null) { + throw new Error('missing peerId parameter') + } + + this._peerIdStr = peerId.toString(base58btc) + this._multiaddrs = multiaddrs + this._onQuery = this._onQuery.bind(this) + } + + start () { + this._mdns = MDNS() + this._mdns.on('query', this._onQuery) + } + + _onQuery (event: QueryPacket, info: RemoteInfo) { + const addresses = this._multiaddrs.reduce((acc, addr) => { + if (addr.isThinWaistAddress()) { + acc.push(addr.toOptions()) + } + return acc + }, []) + + // Only announce TCP for now + if (addresses.length === 0) { + return + } + + const questions = event.questions ?? [] + + // Only respond to queries for our service tag + if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return + + log('got query', event, info) + + const answers: Answer[] = [] + const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}` + + answers.push({ + name: SERVICE_TAG_LOCAL, + type: 'PTR', + class: 'IN', + ttl: 120, + data: peerServiceTagLocal + }) + + // Only announce TCP multiaddrs for now + const port = addresses[0].port + + answers.push({ + name: peerServiceTagLocal, + type: 'SRV', + class: 'IN', + ttl: 120, + data: { + priority: 10, + weight: 1, + port, + target: OS.hostname() + } + }) + + answers.push({ + name: peerServiceTagLocal, + type: 'TXT', + class: 'IN', + ttl: 120, + data: [Buffer.from(this._peerIdStr)] + }) + + addresses.forEach((ma) => { + if ([4, 6].includes(ma.family)) { + answers.push({ + name: OS.hostname(), + type: ma.family === 4 ? 'A' : 'AAAA', + class: 'IN', + ttl: 120, + data: ma.host + }) + } + }) + + if (this._mdns != null) { + log('responding to query', answers) + this._mdns.respond(answers, info) + } + } + + stop () { + if (this._mdns != null) { + this._mdns.removeListener('query', this._onQuery) + return new Promise(resolve => { + if (this._mdns != null) { + this._mdns.destroy(resolve) + } else { + resolve() + } + }) + } + } +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index c55f1a5..0000000 --- a/src/index.js +++ /dev/null @@ -1,146 +0,0 @@ -'use strict' - -const multicastDNS = require('multicast-dns') -const { EventEmitter } = require('events') -const debug = require('debug') -const log = debug('libp2p:mdns') -const query = require('./query') -const GoMulticastDNS = require('./compat') - -class MulticastDNS extends EventEmitter { - constructor (options = {}) { - super() - - if (!options.peerId) { - throw new Error('needs own PeerId to work') - } - - this.broadcast = options.broadcast !== false - this.interval = options.interval || (1e3 * 10) - this.serviceTag = options.serviceTag || 'ipfs.local' - this.port = options.port || 5353 - this.peerId = options.peerId - this.peerMultiaddrs = options.libp2p.multiaddrs || [] - this._queryInterval = null - this._onPeer = this._onPeer.bind(this) - this._onMdnsQuery = this._onMdnsQuery.bind(this) - this._onMdnsResponse = this._onMdnsResponse.bind(this) - - if (options.compat !== false) { - this._goMdns = new GoMulticastDNS({ - multiaddrs: this.peerMultiaddrs, - peerId: options.peerId, - queryPeriod: options.compatQueryPeriod, - queryInterval: options.compatQueryInterval - }) - this._goMdns.on('peer', this._onPeer) - } - } - - /** - * Start sending queries to the LAN. - * - * @returns {void} - */ - async start () { - if (this.mdns) return - - this.mdns = multicastDNS({ port: this.port }) - this.mdns.on('query', this._onMdnsQuery) - this.mdns.on('response', this._onMdnsResponse) - - this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) - - if (this._goMdns) { - await this._goMdns.start() - } - } - - _onMdnsQuery (event) { - query.gotQuery(event, this.mdns, this.peerId, this.peerMultiaddrs, this.serviceTag, this.broadcast) - } - - _onMdnsResponse (event) { - try { - const foundPeer = query.gotResponse(event, this.peerId, this.serviceTag) - - if (foundPeer) { - this.emit('peer', foundPeer) - } - } catch (err) { - log('Error processing peer response', err) - } - } - - _onPeer (peerData) { - this.mdns && this.emit('peer', peerData) - } - - /** - * Stop sending queries to the LAN. - * - * @returns {Promise} - */ - async stop () { - if (!this.mdns) { - return - } - - this.mdns.removeListener('query', this._onMdnsQuery) - this.mdns.removeListener('response', this._onMdnsResponse) - this._goMdns && this._goMdns.removeListener('peer', this._onPeer) - - clearInterval(this._queryInterval) - this._queryInterval = null - - await Promise.all([ - this._goMdns && this._goMdns.stop(), - new Promise((resolve) => this.mdns.destroy(resolve)) - ]) - - this.mdns = undefined - } -} - -exports = module.exports = MulticastDNS -exports.tag = 'mdns' - -/* for reference - - [ { name: 'discovery.ipfs.io.local', - type: 'PTR', - class: 1, - ttl: 120, - data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local' }, - - { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local', - type: 'SRV', - class: 1, - ttl: 120, - data: { priority: 10, weight: 1, port: 4001, target: 'lorien.local' } }, - - { name: 'lorien.local', - type: 'A', - class: 1, - ttl: 120, - data: '127.0.0.1' }, - - { name: 'lorien.local', - type: 'A', - class: 1, - ttl: 120, - data: '127.94.0.1' }, - - { name: 'lorien.local', - type: 'A', - class: 1, - ttl: 120, - data: '172.16.38.224' }, - - { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local', - type: 'TXT', - class: 1, - ttl: 120, - data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' } ], - -*/ diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..023170d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,197 @@ +import multicastDNS from 'multicast-dns' +import { CustomEvent, EventEmitter } from '@libp2p/interfaces' +import { logger } from '@libp2p/logger' +import * as query from './query.js' +import { GoMulticastDNS } from './compat/index.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interfaces/peer-discovery' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { PeerData } from '@libp2p/interfaces/peer-data' + +const log = logger('libp2p:mdns') + +export interface MulticastDNSOptions { + peerId: PeerId + broadcast?: boolean + interval?: number + serviceTag?: string + port?: number + multiaddrs?: Multiaddr[] + compat?: boolean + compatQueryPeriod?: number + compatQueryInterval?: number +} + +export class MulticastDNS extends EventEmitter implements PeerDiscovery { + static tag = 'mdns' + + public mdns?: multicastDNS.MulticastDNS + + private readonly broadcast: boolean + private readonly interval: number + private readonly serviceTag: string + private readonly port: number + private readonly peerId: PeerId + private readonly peerMultiaddrs: Multiaddr[] // TODO: update this when multiaddrs change? + private _queryInterval: NodeJS.Timer | null + private readonly _goMdns?: GoMulticastDNS + + constructor (options: MulticastDNSOptions) { + super() + + if (options.peerId == null) { + throw new Error('needs own PeerId to work') + } + + this.broadcast = options.broadcast !== false + this.interval = options.interval ?? (1e3 * 10) + this.serviceTag = options.serviceTag ?? 'ipfs.local' + this.port = options.port ?? 5353 + this.peerId = options.peerId + this.peerMultiaddrs = options.multiaddrs ?? [] + this._queryInterval = null + this._onPeer = this._onPeer.bind(this) + this._onMdnsQuery = this._onMdnsQuery.bind(this) + this._onMdnsResponse = this._onMdnsResponse.bind(this) + + if (options.compat !== false) { + this._goMdns = new GoMulticastDNS({ + multiaddrs: this.peerMultiaddrs, + peerId: options.peerId, + queryPeriod: options.compatQueryPeriod, + queryInterval: options.compatQueryInterval + }) + this._goMdns.addEventListener('peer', this._onPeer) + } + } + + isStarted () { + return Boolean(this.mdns) + } + + /** + * Start sending queries to the LAN. + * + * @returns {void} + */ + async start () { + if (this.mdns != null) { + return + } + + this.mdns = multicastDNS({ port: this.port }) + this.mdns.on('query', this._onMdnsQuery) + this.mdns.on('response', this._onMdnsResponse) + + this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) + + if (this._goMdns != null) { + await this._goMdns.start() + } + } + + _onMdnsQuery (event: multicastDNS.QueryPacket) { + if (this.mdns == null) { + return + } + + query.gotQuery(event, this.mdns, this.peerId, this.peerMultiaddrs, this.serviceTag, this.broadcast) + } + + _onMdnsResponse (event: multicastDNS.ResponsePacket) { + try { + const foundPeer = query.gotResponse(event, this.peerId, this.serviceTag) + + if (foundPeer != null) { + this.dispatchEvent(new CustomEvent('peer', { + detail: foundPeer + })) + } + } catch (err) { + log('Error processing peer response', err) + } + } + + _onPeer (evt: CustomEvent) { + if (this.mdns == null) { + return + } + + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + } + + /** + * Stop sending queries to the LAN. + * + * @returns {Promise} + */ + async stop () { + if (this.mdns == null) { + return + } + + this.mdns.removeListener('query', this._onMdnsQuery) + this.mdns.removeListener('response', this._onMdnsResponse) + this._goMdns?.removeEventListener('peer', this._onPeer) + + if (this._queryInterval != null) { + clearInterval(this._queryInterval) + this._queryInterval = null + } + + await Promise.all([ + this._goMdns?.stop(), + new Promise((resolve) => { + if (this.mdns != null) { + this.mdns.destroy(resolve) + } else { + resolve() + } + }) + ]) + + this.mdns = undefined + } +} + +/* for reference + + [ { name: 'discovery.ipfs.io.local', + type: 'PTR', + class: 1, + ttl: 120, + data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local' }, + + { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local', + type: 'SRV', + class: 1, + ttl: 120, + data: { priority: 10, weight: 1, port: 4001, target: 'lorien.local' } }, + + { name: 'lorien.local', + type: 'A', + class: 1, + ttl: 120, + data: '127.0.0.1' }, + + { name: 'lorien.local', + type: 'A', + class: 1, + ttl: 120, + data: '127.94.0.1' }, + + { name: 'lorien.local', + type: 'A', + class: 1, + ttl: 120, + data: '172.16.38.224' }, + + { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local', + type: 'TXT', + class: 1, + ttl: 120, + data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' } ], + +*/ diff --git a/src/query.js b/src/query.js deleted file mode 100644 index 398d9be..0000000 --- a/src/query.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict' - -const os = require('os') -const debug = require('debug') -const log = debug('libp2p:mdns') -log.error = debug('libp2p:mdns:error') -const { Multiaddr } = require('multiaddr') -const Id = require('peer-id') - -module.exports = { - - queryLAN: function (mdns, serviceTag, interval) { - const query = () => { - log('query', serviceTag) - mdns.query({ - questions: [{ - name: serviceTag, - type: 'PTR' - }] - }) - } - - // Immediately start a query, then do it every interval. - query() - return setInterval(query, interval) - }, - - gotResponse: function (rsp, localPeerId, serviceTag) { - if (!rsp.answers) { return } - - const answers = { - ptr: {}, - srv: {}, - txt: {}, - a: [], - aaaa: [] - } - - rsp.answers.forEach((answer) => { - switch (answer.type) { - case 'PTR': answers.ptr = answer; break - case 'SRV': answers.srv = answer; break - case 'TXT': answers.txt = answer; break - case 'A': answers.a.push(answer); break - case 'AAAA': answers.aaaa.push(answer); break - default: break - } - }) - - if (answers.ptr.name !== serviceTag) { - return - } - - const b58Id = answers.txt.data[0].toString() - const port = answers.srv.data.port - const multiaddrs = [] - - answers.a.forEach((a) => { - const ma = new Multiaddr('/ip4/' + a.data + '/tcp/' + port) - - if (!multiaddrs.some((m) => m.equals(ma))) { - multiaddrs.push(ma) - } - }) - - answers.aaaa.forEach((a) => { - const ma = new Multiaddr('/ip6/' + a.data + '/tcp/' + port) - - if (!multiaddrs.some((m) => m.equals(ma))) { - multiaddrs.push(ma) - } - }) - - if (localPeerId.toB58String() === b58Id) { - return // replied to myself, ignore - } - - log('peer found -', b58Id) - - return { - id: Id.createFromB58String(b58Id), - multiaddrs - } - }, - - gotQuery: function (qry, mdns, peerId, multiaddrs, serviceTag, broadcast) { - if (!broadcast) { return } - - const addresses = multiaddrs.reduce((acc, addr) => { - if (addr.isThinWaistAddress()) { - acc.push(addr.toOptions()) - } - return acc - }, []) - - // Only announce TCP for now - if (addresses.length === 0) { return } - - if (qry.questions[0] && qry.questions[0].name === serviceTag) { - const answers = [] - - answers.push({ - name: serviceTag, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerId.toB58String() + '.' + serviceTag - }) - - // Only announce TCP multiaddrs for now - const port = addresses[0].port - - answers.push({ - name: peerId.toB58String() + '.' + serviceTag, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port: port, - target: os.hostname() - } - }) - - answers.push({ - name: peerId.toB58String() + '.' + serviceTag, - type: 'TXT', - class: 'IN', - ttl: 120, - data: peerId.toB58String() - }) - - addresses.forEach((addr) => { - if ([4, 6].includes(addr.family)) { - answers.push({ - name: os.hostname(), - type: addr.family === 4 ? 'A' : 'AAAA', - class: 'IN', - ttl: 120, - data: addr.host - }) - } - }) - - log('responding to query') - mdns.respond(answers) - } - } -} diff --git a/src/query.ts b/src/query.ts new file mode 100644 index 0000000..6f5de39 --- /dev/null +++ b/src/query.ts @@ -0,0 +1,164 @@ +import os from 'os' +import { logger } from '@libp2p/logger' +import { Multiaddr, MultiaddrObject } from '@multiformats/multiaddr' +import { base58btc } from 'multiformats/bases/base58' +import { PeerId } from '@libp2p/peer-id' +import type { PeerData } from '@libp2p/interfaces/peer-data' +import type { MulticastDNS, ResponsePacket, QueryPacket } from 'multicast-dns' +import type { SrvAnswer, StringAnswer, TxtAnswer, Answer } from 'dns-packet' + +const log = logger('libp2p:mdns') + +export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: number) { + const query = () => { + log('query', serviceTag) + mdns.query({ + questions: [{ + name: serviceTag, + type: 'PTR' + }] + }) + } + + // Immediately start a query, then do it every interval. + query() + return setInterval(query, interval) +} + +interface Answers { + ptr?: StringAnswer + srv?: SrvAnswer + txt?: TxtAnswer + a: StringAnswer[] + aaaa: StringAnswer[] +} + +export function gotResponse (rsp: ResponsePacket, localPeerId: PeerId, serviceTag: string): PeerData | undefined { + if (rsp.answers == null) { + return + } + + const answers: Answers = { + a: [], + aaaa: [] + } + + rsp.answers.forEach((answer) => { + switch (answer.type) { + case 'PTR': answers.ptr = answer; break + case 'SRV': answers.srv = answer; break + case 'TXT': answers.txt = answer; break + case 'A': answers.a.push(answer); break + case 'AAAA': answers.aaaa.push(answer); break + default: break + } + }) + + if (answers.ptr == null || + answers.ptr.name !== serviceTag || + answers.txt == null || + answers.srv == null) { + return + } + + const b58Id = answers.txt.data[0].toString() + const port = answers.srv.data.port + const multiaddrs: Multiaddr[] = [] + + answers.a.forEach((a) => { + const ma = new Multiaddr(`/ip4/${a.data}/tcp/${port}`) + + if (!multiaddrs.some((m) => m.equals(ma))) { + multiaddrs.push(ma) + } + }) + + answers.aaaa.forEach((a) => { + const ma = new Multiaddr(`/ip6/${a.data}/tcp/${port}`) + + if (!multiaddrs.some((m) => m.equals(ma))) { + multiaddrs.push(ma) + } + }) + + if (localPeerId.toString(base58btc) === b58Id) { + return // replied to myself, ignore + } + + log('peer found -', b58Id) + + return { + id: PeerId.fromString(b58Id), + multiaddrs, + protocols: [] + } +} + +export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerId: PeerId, multiaddrs: Multiaddr[], serviceTag: string, broadcast: boolean) { + if (!broadcast) { + return + } + + const addresses: MultiaddrObject[] = multiaddrs.reduce((acc, addr) => { + if (addr.isThinWaistAddress()) { + acc.push(addr.toOptions()) + } + return acc + }, []) + + // Only announce TCP for now + if (addresses.length === 0) { + return + } + + if (qry.questions[0] != null && qry.questions[0].name === serviceTag) { + const answers: Answer[] = [] + + answers.push({ + name: serviceTag, + type: 'PTR', + class: 'IN', + ttl: 120, + data: peerId.toString(base58btc) + '.' + serviceTag + }) + + // Only announce TCP multiaddrs for now + const port = addresses[0].port + + answers.push({ + name: peerId.toString(base58btc) + '.' + serviceTag, + type: 'SRV', + class: 'IN', + ttl: 120, + data: { + priority: 10, + weight: 1, + port: port, + target: os.hostname() + } + }) + + answers.push({ + name: peerId.toString(base58btc) + '.' + serviceTag, + type: 'TXT', + class: 'IN', + ttl: 120, + data: peerId.toString(base58btc) + }) + + addresses.forEach((addr) => { + if ([4, 6].includes(addr.family)) { + answers.push({ + name: os.hostname(), + type: addr.family === 4 ? 'A' : 'AAAA', + class: 'IN', + ttl: 120, + data: addr.host + }) + } + }) + + log('responding to query') + mdns.respond(answers) + } +} diff --git a/test/compat/go-multicast-dns.spec.js b/test/compat/go-multicast-dns.spec.ts similarity index 69% rename from test/compat/go-multicast-dns.spec.js rename to test/compat/go-multicast-dns.spec.ts index 6f63705..8473dc9 100644 --- a/test/compat/go-multicast-dns.spec.js +++ b/test/compat/go-multicast-dns.spec.ts @@ -1,24 +1,22 @@ /* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') -const pDefer = require('p-defer') - -const GoMulticastDNS = require('../../src/compat') +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import pDefer from 'p-defer' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import { GoMulticastDNS } from '../../src/compat/index.js' describe('GoMulticastDNS', () => { const peerAddrs = [ new Multiaddr('/ip4/127.0.0.1/tcp/20001'), new Multiaddr('/ip4/127.0.0.1/tcp/20002') ] - let peerIds + let peerIds: PeerId[] before(async () => { peerIds = await Promise.all([ - PeerId.create(), - PeerId.create() + createEd25519PeerId(), + createEd25519PeerId() ]) }) @@ -29,7 +27,7 @@ describe('GoMulticastDNS', () => { }) await mdns.start() - return mdns.stop() + return await mdns.stop() }) it('should ignore multiple start calls', async () => { @@ -41,7 +39,7 @@ describe('GoMulticastDNS', () => { await mdns.start() await mdns.start() - return mdns.stop() + return await mdns.stop() }) it('should ignore unnecessary stop calls', async () => { @@ -63,15 +61,19 @@ describe('GoMulticastDNS', () => { }) const defer = pDefer() - mdnsA.on('peer', ({ id, multiaddrs }) => { - if (!id.isEqual(peerIds[1])) return + mdnsA.addEventListener('peer', (evt) => { + const { id, multiaddrs } = evt.detail + + if (!peerIds[1].equals(id)) { + return + } expect(multiaddrs.some((m) => m.equals(peerAddrs[1]))).to.be.true() defer.resolve() }) // Start in series - Promise.all([ + void Promise.all([ mdnsA.start(), mdnsB.start() ]) diff --git a/test/compat/querier.spec.js b/test/compat/querier.spec.ts similarity index 60% rename from test/compat/querier.spec.js rename to test/compat/querier.spec.ts index 2268a45..1737680 100644 --- a/test/compat/querier.spec.js +++ b/test/compat/querier.spec.ts @@ -1,34 +1,36 @@ /* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const PeerId = require('peer-id') -const MDNS = require('multicast-dns') -const OS = require('os') -const delay = require('delay') - -const Querier = require('../../src/compat/querier') -const { SERVICE_TAG_LOCAL } = require('../../src/compat/constants') +import { expect } from 'aegir/utils/chai.js' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import MDNS, { QueryPacket } from 'multicast-dns' +import OS from 'os' +import delay from 'delay' +import { Querier } from '../../src/compat/querier.js' +import { SERVICE_TAG_LOCAL } from '../../src/compat/constants.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import { base58btc } from 'multiformats/bases/base58' +import type { RemoteInfo } from 'dgram' +import type { Answer } from 'dns-packet' describe('Querier', () => { - let querier, mdns + let querier: Querier + let mdns: MDNS.MulticastDNS const peerAddrs = [ '/ip4/127.0.0.1/tcp/20001', '/ip4/127.0.0.1/tcp/20002' ] - let peerIds + let peerIds: PeerId[] before(async () => { peerIds = await Promise.all([ - PeerId.create(), - PeerId.create() + createEd25519PeerId(), + createEd25519PeerId() ]) }) - afterEach(() => { - return Promise.all([ - querier && querier.stop(), - mdns && mdns.destroy() + afterEach(async () => { + return await Promise.all([ + querier?.stop(), + mdns?.destroy() ]) }) @@ -46,7 +48,7 @@ describe('Querier', () => { let queryCount = 0 mdns.on('query', event => { - const questions = event.questions || [] + const questions = event.questions ?? [] if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return queryCount++ }) @@ -57,9 +59,9 @@ describe('Querier', () => { expect(queryCount >= 2).to.be.true() }) - it('should not emit peer for responses with non matching service tags', () => { - return ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should not emit peer for responses with non matching service tags', async () => { + return await ensureNoPeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` const bogusServiceTagLocal = '_ifps-discovery._udp' return [{ @@ -72,9 +74,9 @@ describe('Querier', () => { }) }) - it('should not emit peer for responses with missing TXT record', () => { - return ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should not emit peer for responses with missing TXT record', async () => { + return await ensureNoPeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -86,9 +88,9 @@ describe('Querier', () => { }) }) - it('should not emit peer for responses with missing peer ID in TXT record', () => { - return ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should not emit peer for responses with missing peer ID in TXT record', async () => { + return await ensureNoPeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -106,9 +108,9 @@ describe('Querier', () => { }) }) - it('should not emit peer for responses to self', () => { - return ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should not emit peer for responses to self', async () => { + return await ensureNoPeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -121,15 +123,15 @@ describe('Querier', () => { type: 'TXT', class: 'IN', ttl: 120, - data: peerIds[0].toB58String() + data: peerIds[0].toString(base58btc) }] }) }) // TODO: unskip when https://github.com/libp2p/js-peer-id/issues/83 is resolved - it.skip('should not emit peer for responses with invalid peer ID in TXT record', () => { - return ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it.skip('should not emit peer for responses with invalid peer ID in TXT record', async () => { + return await ensureNoPeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -147,9 +149,9 @@ describe('Querier', () => { }) }) - it('should not emit peer for responses with missing SRV record', () => { - return ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should not emit peer for responses with missing SRV record', async () => { + return await ensureNoPeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -162,14 +164,14 @@ describe('Querier', () => { type: 'TXT', class: 'IN', ttl: 120, - data: peerIds[1].toB58String() + data: peerIds[1].toString(base58btc) }] }) }) - it('should emit peer for responses even if no multiaddrs', () => { - return ensurePeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should emit peer for responses even if no multiaddrs', async () => { + return await ensurePeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -182,7 +184,7 @@ describe('Querier', () => { type: 'TXT', class: 'IN', ttl: 120, - data: peerIds[1].toB58String() + data: peerIds[1].toString(base58btc) }, { name: peerServiceTagLocal, type: 'SRV', @@ -191,16 +193,16 @@ describe('Querier', () => { data: { priority: 10, weight: 1, - port: parseInt(peerAddrs[1].split().pop()), + port: parseInt(peerAddrs[1].split('').pop() ?? '0'), target: OS.hostname() } }] }) }) - it('should emit peer for responses with valid multiaddrs', () => { - return ensurePeer(event => { - const peerServiceTagLocal = `${peerIds[1].toB58String()}.${SERVICE_TAG_LOCAL}` + it('should emit peer for responses with valid multiaddrs', async () => { + return await ensurePeer(event => { + const peerServiceTagLocal = `${peerIds[1].toString(base58btc)}.${SERVICE_TAG_LOCAL}` return [{ name: SERVICE_TAG_LOCAL, @@ -213,7 +215,7 @@ describe('Querier', () => { type: 'TXT', class: 'IN', ttl: 120, - data: peerIds[1].toB58String() + data: peerIds[1].toString(base58btc) }, { name: peerServiceTagLocal, type: 'SRV', @@ -222,7 +224,7 @@ describe('Querier', () => { data: { priority: 10, weight: 1, - port: parseInt(peerAddrs[1].split().pop()), + port: parseInt(peerAddrs[1].split('').pop() ?? '0'), target: OS.hostname() } }, { @@ -240,27 +242,33 @@ describe('Querier', () => { * * @param {Function} getResponse - Given a query, construct a response to test the querier */ - async function ensurePeer (getResponse) { + async function ensurePeer (getResponse: (event: QueryPacket, info: RemoteInfo) => Answer[]) { querier = new Querier({ peerId: peerIds[0] }) mdns = MDNS() mdns.on('query', (event, info) => { - const questions = event.questions || [] + const questions = event.questions ?? [] if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return mdns.respond(getResponse(event, info), info) }) let peerId - querier.on('peer', ({ id }) => { + querier.addEventListener('peer', (evt) => { + const { id } = evt.detail + // Ignore non-test peers - if (!id.isEqual(peerIds[1])) return + if (!peerIds[1].equals(id)) { + return + } peerId = id }) await querier.start() await delay(100) - if (!peerId) throw new Error('Missing peer') + if (peerId == null) { + throw new Error('Missing peer') + } } /** @@ -268,28 +276,40 @@ describe('Querier', () => { * * @param {Function} getResponse - Given a query, construct a response to test the querier */ - async function ensureNoPeer (getResponse) { + async function ensureNoPeer (getResponse: (event: QueryPacket, info: RemoteInfo) => Answer[]) { querier = new Querier({ peerId: peerIds[0] }) mdns = MDNS() mdns.on('query', (event, info) => { - const questions = event.questions || [] - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return + const questions = event.questions ?? [] + + if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) { + return + } + mdns.respond(getResponse(event, info), info) }) let peerId - querier.on('peer', ({ id }) => { + querier.addEventListener('peer', (evt) => { + const { id } = evt.detail + // Ignore non-test peers - if (!id.isEqual(peerIds[0]) && !id.isEqual(peerIds[1])) return + if (!peerIds[0].equals(id) && !peerIds[1].equals(id)) { + return + } + peerId = id }) await querier.start() await delay(100) - if (!peerId) return + if (peerId == null) { + return + } + throw Object.assign(new Error('Unexpected peer'), { peerId }) } }) diff --git a/test/compat/responder.spec.js b/test/compat/responder.spec.ts similarity index 63% rename from test/compat/responder.spec.js rename to test/compat/responder.spec.ts index 2be2320..bfba7ac 100644 --- a/test/compat/responder.spec.js +++ b/test/compat/responder.spec.ts @@ -1,35 +1,37 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') -const MDNS = require('multicast-dns') -const delay = require('delay') -const pDefer = require('p-defer') - -const Responder = require('../../src/compat/responder') -const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('../../src/compat/constants') +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import MDNS from 'multicast-dns' +import delay from 'delay' +import pDefer from 'p-defer' +import { Responder } from '../../src/compat/responder.js' +import { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } from '../../src/compat/constants.js' +import { base58btc } from 'multiformats/bases/base58' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { ResponsePacket } from 'multicast-dns' describe('Responder', () => { - let responder, mdns + let responder: Responder + let mdns: MDNS.MulticastDNS const peerAddrs = [ new Multiaddr('/ip4/127.0.0.1/tcp/20001'), new Multiaddr('/ip4/127.0.0.1/tcp/20002') ] - let peerIds + let peerIds: PeerId[] before(async () => { peerIds = await Promise.all([ - PeerId.create(), - PeerId.create() + createEd25519PeerId(), + createEd25519PeerId() ]) }) - afterEach(() => { - return Promise.all([ - responder && responder.stop(), - mdns && mdns.destroy() + afterEach(async () => { + return await Promise.all([ + responder?.stop(), + mdns?.destroy() ]) }) @@ -44,7 +46,7 @@ describe('Responder', () => { }) it('should not respond to a query if no TCP addresses', async () => { - const peerId = await PeerId.create() + const peerId = await createEd25519PeerId() responder = new Responder({ peerId, multiaddrs: [] @@ -64,7 +66,7 @@ describe('Responder', () => { mdns.query({ id: 1, // id > 0 for unicast response questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] - }, null, { + }, { address: MULTICAST_IP, port: MULTICAST_PORT }) @@ -95,7 +97,7 @@ describe('Responder', () => { mdns.query({ id: 1, // id > 0 for unicast response questions: [{ name: bogusServiceTagLocal, type: 'PTR', class: 'IN' }] - }, null, { + }, { address: MULTICAST_IP, port: MULTICAST_PORT }) @@ -115,20 +117,30 @@ describe('Responder', () => { const defer = pDefer() mdns.on('response', event => { - if (!isResponseFrom(event, peerIds[0])) return + if (!isResponseFrom(event, peerIds[0])) { + return + } const srvRecord = event.answers.find(a => a.type === 'SRV') - if (!srvRecord) return defer.reject(new Error('Missing SRV record')) + if (srvRecord == null || srvRecord.type !== 'SRV') { + return defer.reject(new Error('Missing SRV record')) + } - const { port } = srvRecord.data || {} + const { port } = srvRecord.data ?? {} const protos = { A: 'ip4', AAAA: 'ip6' } const addrs = event.answers .filter(a => ['A', 'AAAA'].includes(a.type)) - .map(a => `/${protos[a.type]}/${a.data}/tcp/${port}`) + .map(a => { + if (a.type !== 'A' && a.type !== 'AAAA') { + throw new Error('Incorrect type') + } + + return `/${protos[a.type]}/${a.data}/tcp/${port}` + }) if (!addrs.includes(peerAddrs[0].toString())) { - return defer.reject(new Error('Missing peer address in response: ' + peerAddrs[0])) + return defer.reject(new Error(`Missing peer address in response: ${peerAddrs[0].toString()}`)) } defer.resolve() @@ -137,7 +149,7 @@ describe('Responder', () => { mdns.query({ id: 1, // id > 0 for unicast response questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] - }, null, { + }, { address: MULTICAST_IP, port: MULTICAST_PORT }) @@ -146,13 +158,15 @@ describe('Responder', () => { }) }) -function isResponseFrom (res, fromPeerId) { - const answers = res.answers || [] +function isResponseFrom (res: ResponsePacket, fromPeerId: PeerId) { + const answers = res.answers ?? [] const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) - if (!ptrRecord) return false // Ignore irrelevant + if (ptrRecord == null) return false // Ignore irrelevant const txtRecord = answers.find(a => a.type === 'TXT') - if (!txtRecord) return false // Ignore missing TXT record + if ((txtRecord == null) || txtRecord.type !== 'TXT') { + return false // Ignore missing TXT record + } let peerIdStr try { @@ -162,7 +176,7 @@ function isResponseFrom (res, fromPeerId) { } // Ignore response from someone else - if (fromPeerId.toB58String() !== peerIdStr) return false + if (fromPeerId.toString(base58btc) !== peerIdStr) return false return true } diff --git a/test/compliance.spec.js b/test/compliance.spec.js deleted file mode 100644 index c590c32..0000000 --- a/test/compliance.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -/* eslint-env mocha */ - -const tests = require('libp2p-interfaces-compliance-tests/src/peer-discovery') - -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') -const MulticastDNS = require('../src') -let mdns - -describe('compliance tests', () => { - let intervalId - tests({ - async setup () { - const peerId1 = await PeerId.create() - const peerId2 = await PeerId.create() - - mdns = new MulticastDNS({ - peerId: peerId1, - libp2p: { - multiaddrs: [] - }, - broadcast: false, - port: 50001, - compat: true - }) - - // Trigger discovery - const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d' - intervalId = setInterval(() => mdns._onPeer({ - id: peerId2, - multiaddrs: [new Multiaddr(maStr)] - }), 1000) - - return mdns - }, - async teardown () { - clearInterval(intervalId) - await new Promise(resolve => setTimeout(resolve, 10)) - } - }) -}) diff --git a/test/compliance.spec.ts b/test/compliance.spec.ts new file mode 100644 index 0000000..13bf6eb --- /dev/null +++ b/test/compliance.spec.ts @@ -0,0 +1,44 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-compliance-tests/peer-discovery' +import { Multiaddr } from '@multiformats/multiaddr' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { MulticastDNS } from '../src/index.js' +import { CustomEvent } from '@libp2p/interfaces' + +let mdns: MulticastDNS + +describe('compliance tests', () => { + let intervalId: NodeJS.Timer + + tests({ + async setup () { + const peerId1 = await createEd25519PeerId() + const peerId2 = await createEd25519PeerId() + + mdns = new MulticastDNS({ + peerId: peerId1, + multiaddrs: [], + broadcast: false, + port: 50001, + compat: true + }) + + // Trigger discovery + const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d' + intervalId = setInterval(() => mdns._onPeer(new CustomEvent('peer', { + detail: { + id: peerId2, + multiaddrs: [new Multiaddr(maStr)], + protocols: [] + } + })), 1000) + + return mdns + }, + async teardown () { + clearInterval(intervalId) + await new Promise(resolve => setTimeout(resolve, 10)) + } + }) +}) diff --git a/test/multicast-dns.spec.js b/test/multicast-dns.spec.ts similarity index 63% rename from test/multicast-dns.spec.js rename to test/multicast-dns.spec.ts index bd1ff14..8603fa3 100644 --- a/test/multicast-dns.spec.js +++ b/test/multicast-dns.spec.ts @@ -1,27 +1,32 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') -const pWaitFor = require('p-wait-for') - -const MulticastDNS = require('./../src') +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import pWaitFor from 'p-wait-for' +import { MulticastDNS } from './../src/index.js' +import { base58btc } from 'multiformats/bases/base58' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { PeerData } from '@libp2p/interfaces/peer-data' describe('MulticastDNS', () => { - let pA, aMultiaddrs - let pB, bMultiaddrs - let pC, cMultiaddrs - let pD, dMultiaddrs + let pA: PeerId + let aMultiaddrs: Multiaddr[] + let pB: PeerId + let bMultiaddrs: Multiaddr[] + let pC: PeerId + let cMultiaddrs: Multiaddr[] + let pD: PeerId + let dMultiaddrs: Multiaddr[] before(async function () { this.timeout(80 * 1000) ;[pA, pB, pC, pD] = await Promise.all([ - PeerId.create(), - PeerId.create(), - PeerId.create(), - PeerId.create() + createEd25519PeerId(), + createEd25519PeerId(), + createEd25519PeerId(), + createEd25519PeerId() ]) aMultiaddrs = [ @@ -52,9 +57,7 @@ describe('MulticastDNS', () => { const mdnsA = new MulticastDNS({ peerId: pA, - libp2p: { - multiaddrs: aMultiaddrs - }, + multiaddrs: aMultiaddrs, broadcast: false, // do not talk to ourself port: 50001, compat: false @@ -62,19 +65,19 @@ describe('MulticastDNS', () => { const mdnsB = new MulticastDNS({ peerId: pB, - libp2p: { - multiaddrs: bMultiaddrs - }, + multiaddrs: bMultiaddrs, port: 50001, // port must be the same compat: false }) - mdnsA.start() - mdnsB.start() + await mdnsA.start() + await mdnsB.start() - const { id } = await new Promise((resolve) => mdnsA.once('peer', resolve)) + const { detail: { id } } = await new Promise>((resolve) => mdnsA.addEventListener('peer', resolve, { + once: true + })) - expect(pB.toB58String()).to.eql(id.toB58String()) + expect(pB.toString(base58btc)).to.eql(id.toString(base58btc)) await Promise.all([mdnsA.stop(), mdnsB.stop()]) }) @@ -84,42 +87,36 @@ describe('MulticastDNS', () => { const mdnsA = new MulticastDNS({ peerId: pA, - libp2p: { - multiaddrs: aMultiaddrs - }, + multiaddrs: aMultiaddrs, broadcast: false, // do not talk to ourself port: 50003, compat: false }) const mdnsC = new MulticastDNS({ peerId: pC, - libp2p: { - multiaddrs: cMultiaddrs - }, + multiaddrs: cMultiaddrs, port: 50003, // port must be the same compat: false }) const mdnsD = new MulticastDNS({ peerId: pD, - libp2p: { - multiaddrs: dMultiaddrs - }, + multiaddrs: dMultiaddrs, port: 50003, // port must be the same compat: false }) - mdnsA.start() - mdnsC.start() - mdnsD.start() + await mdnsA.start() + await mdnsC.start() + await mdnsD.start() const peers = new Map() - const expectedPeer = pC.toB58String() + const expectedPeer = pC.toString(base58btc) - const foundPeer = peer => peers.set(peer.id.toB58String(), peer) - mdnsA.on('peer', foundPeer) + const foundPeer = (evt: CustomEvent) => peers.set(evt.detail.id.toString(base58btc), evt.detail) + mdnsA.addEventListener('peer', foundPeer) await pWaitFor(() => peers.has(expectedPeer)) - mdnsA.removeListener('peer', foundPeer) + mdnsA.removeEventListener('peer', foundPeer) expect(peers.get(expectedPeer).multiaddrs.length).to.equal(1) @@ -135,9 +132,7 @@ describe('MulticastDNS', () => { const mdnsA = new MulticastDNS({ peerId: pA, - libp2p: { - multiaddrs: aMultiaddrs - }, + multiaddrs: aMultiaddrs, broadcast: false, // do not talk to ourself port: 50001, compat: false @@ -145,19 +140,19 @@ describe('MulticastDNS', () => { const mdnsB = new MulticastDNS({ peerId: pB, - libp2p: { - multiaddrs: bMultiaddrs - }, + multiaddrs: bMultiaddrs, port: 50001, compat: false }) - mdnsA.start() - mdnsB.start() + await mdnsA.start() + await mdnsB.start() - const { id, multiaddrs } = await new Promise((resolve) => mdnsA.once('peer', resolve)) + const { detail: { id, multiaddrs } } = await new Promise>((resolve) => mdnsA.addEventListener('peer', resolve, { + once: true + })) - expect(pB.toB58String()).to.eql(id.toB58String()) + expect(pB.toString(base58btc)).to.eql(id.toString(base58btc)) expect(multiaddrs.length).to.equal(2) await Promise.all([mdnsA.stop(), mdnsB.stop()]) @@ -168,29 +163,27 @@ describe('MulticastDNS', () => { const mdnsA = new MulticastDNS({ peerId: pA, - libp2p: { - multiaddrs: aMultiaddrs - }, + multiaddrs: aMultiaddrs, port: 50004, // port must be the same compat: false }) const mdnsC = new MulticastDNS({ peerId: pC, - libp2p: { - multiaddrs: cMultiaddrs - }, + multiaddrs: cMultiaddrs, port: 50004, compat: false }) - mdnsA.start() + await mdnsA.start() await new Promise((resolve) => setTimeout(resolve, 1000)) await mdnsA.stop() - mdnsC.start() + await mdnsC.start() - mdnsC.once('peer', () => { + mdnsC.addEventListener('peer', () => { throw new Error('Should not receive new peer.') + }, { + once: true }) await new Promise((resolve) => setTimeout(resolve, 5000)) @@ -200,9 +193,7 @@ describe('MulticastDNS', () => { it('should start and stop with go-libp2p-mdns compat', async () => { const mdns = new MulticastDNS({ peerId: pA, - libp2p: { - multiaddrs: aMultiaddrs - }, + multiaddrs: aMultiaddrs, port: 50004 }) @@ -213,20 +204,23 @@ describe('MulticastDNS', () => { it('should not emit undefined peer ids', async () => { const mdns = new MulticastDNS({ peerId: pA, - libp2p: { - multiaddrs: aMultiaddrs - }, + multiaddrs: aMultiaddrs, port: 50004 }) await mdns.start() - await new Promise((resolve, reject) => { - mdns.on('peer', (peerData) => { - if (!peerData) { + await new Promise((resolve, reject) => { + mdns.addEventListener('peer', (evt) => { + if (evt.detail == null) { reject(new Error('peerData was not set')) } }) + if (mdns.mdns == null) { + reject(new Error('mdns property was not set')) + return + } + mdns.mdns.on('response', () => { // query.gotResponse is async - we'll bail from that method when // comparing the senders PeerId to our own but it'll happen later diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f296f99 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ] +}