From 78b61a266370852aed3c72ebed1c0f498885da8b Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 10 Oct 2023 12:59:42 +0100 Subject: [PATCH 1/9] docs: add migration guide --- doc/migrations/v0.46-v1.0.0.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/migrations/v0.46-v1.0.0.md diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md new file mode 100644 index 0000000000..a8b22de3e5 --- /dev/null +++ b/doc/migrations/v0.46-v1.0.0.md @@ -0,0 +1,16 @@ +# Migrating to libp2p@1.0.0 + +A migration guide for refactoring your application code from libp2p `v0.46` to `v1.0.0`. + +## Table of Contents + +- [New features](#new-features) +- [Breaking changes](#breaking-changes) + +## New features + +... + +## Breaking changes + +... From 7f2ed7f0e555dbdb8180b96ea335ccaffd2d9de4 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Wed, 11 Oct 2023 19:21:12 +0100 Subject: [PATCH 2/9] fix!: remove dialler language (#2143) Co-authored-by: Chad Nehemiah Co-authored-by: Cayman --- doc/migrations/v0.46-v1.0.md | 53 +++++++++++++++++++ .../src/connection-manager/dial-queue.ts | 4 +- .../test/connection-manager/auto-dial.spec.ts | 40 +++++++------- 3 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 doc/migrations/v0.46-v1.0.md diff --git a/doc/migrations/v0.46-v1.0.md b/doc/migrations/v0.46-v1.0.md new file mode 100644 index 0000000000..184b266cf0 --- /dev/null +++ b/doc/migrations/v0.46-v1.0.md @@ -0,0 +1,53 @@ + +# Migrating to libp2p@1.0 + +A migration guide for refactoring your application code from libp2p v0.46 to v1.0. + +## Table of Contents + +- [API](#api) +- [Module Updates](#module-updates) +- [Metrics](#metrics) + +## API + + + +## Module Updates + +With this release you should update the following libp2p modules if you are relying on them: + + + +```json + +``` + +## Metrics + +The following metrics were renamed: + +`libp2p_dialler_pending_dials` => `libp2p_dial_queue_pending_dials` +`libp2p_dialler_in_progress_dials` => `libp2p_dial_queue_in_progress_dials` diff --git a/packages/libp2p/src/connection-manager/dial-queue.ts b/packages/libp2p/src/connection-manager/dial-queue.ts index 280e677c02..a724796406 100644 --- a/packages/libp2p/src/connection-manager/dial-queue.ts +++ b/packages/libp2p/src/connection-manager/dial-queue.ts @@ -103,8 +103,8 @@ export class DialQueue { setMaxListeners(Infinity, this.shutDownController.signal) - this.pendingDialCount = components.metrics?.registerMetric('libp2p_dialler_pending_dials') - this.inProgressDialCount = components.metrics?.registerMetric('libp2p_dialler_in_progress_dials') + this.pendingDialCount = components.metrics?.registerMetric('libp2p_dial_queue_pending_dials') + this.inProgressDialCount = components.metrics?.registerMetric('libp2p_dial_queue_in_progress_dials') this.pendingDials = [] for (const [key, value] of Object.entries(init.resolvers ?? {})) { diff --git a/packages/libp2p/test/connection-manager/auto-dial.spec.ts b/packages/libp2p/test/connection-manager/auto-dial.spec.ts index 9e45ae8c7b..9019890e61 100644 --- a/packages/libp2p/test/connection-manager/auto-dial.spec.ts +++ b/packages/libp2p/test/connection-manager/auto-dial.spec.ts @@ -22,7 +22,7 @@ import type { PeerStore, Peer } from '@libp2p/interface/peer-store' import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' describe('auto-dial', () => { - let autoDialler: AutoDial + let autoDialer: AutoDial let events: TypedEventTarget let peerStore: PeerStore let peerId: PeerId @@ -38,8 +38,8 @@ describe('auto-dial', () => { }) afterEach(() => { - if (autoDialler != null) { - autoDialler.stop() + if (autoDialer != null) { + autoDialer.stop() } }) @@ -73,7 +73,7 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events @@ -81,8 +81,8 @@ describe('auto-dial', () => { minConnections: 10, autoDialInterval: 10000 }) - autoDialler.start() - void autoDialler.autoDial() + autoDialer.start() + void autoDialer.autoDial() await pWaitFor(() => { return connectionManager.openConnection.callCount === 1 @@ -127,15 +127,15 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events }, { minConnections: 10 }) - autoDialler.start() - await autoDialler.autoDial() + autoDialer.start() + await autoDialer.autoDial() await pWaitFor(() => connectionManager.openConnection.callCount === 1) await delay(1000) @@ -181,15 +181,15 @@ describe('auto-dial', () => { }]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events }, { minConnections: 10 }) - autoDialler.start() - await autoDialler.autoDial() + autoDialer.start() + await autoDialer.autoDial() await pWaitFor(() => connectionManager.openConnection.callCount === 1) await delay(1000) @@ -207,7 +207,7 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events @@ -215,12 +215,12 @@ describe('auto-dial', () => { minConnections: 10, autoDialInterval: 10000 }) - autoDialler.start() + autoDialer.start() // call autodial twice await Promise.all([ - autoDialler.autoDial(), - autoDialler.autoDial() + autoDialer.autoDial(), + autoDialer.autoDial() ]) // should only have queried peer store once @@ -258,7 +258,7 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events @@ -266,9 +266,9 @@ describe('auto-dial', () => { minConnections: 10, autoDialPeerRetryThreshold: 2000 }) - autoDialler.start() + autoDialer.start() - void autoDialler.autoDial() + void autoDialer.autoDial() await pWaitFor(() => { return connectionManager.openConnection.callCount === 1 @@ -282,7 +282,7 @@ describe('auto-dial', () => { await delay(2000) // autodial again - void autoDialler.autoDial() + void autoDialer.autoDial() await pWaitFor(() => { return connectionManager.openConnection.callCount === 3 From eec1294d074577db378bb85a70026edee08968cc Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Fri, 13 Oct 2023 19:43:16 +0300 Subject: [PATCH 3/9] fix!: remove min/max from topologies (#2158) Once upon a time these options were intended to cause libp2p to close connections or search for more peers that support a given protocol but it was never implemented. Remove the options since they don't do anything, they may be restored in future if the functionality is ever required. --- packages/interface/src/topology/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/interface/src/topology/index.ts b/packages/interface/src/topology/index.ts index 25c321201b..76bb64c1ee 100644 --- a/packages/interface/src/topology/index.ts +++ b/packages/interface/src/topology/index.ts @@ -2,9 +2,6 @@ import type { Connection } from '../connection/index.js' import type { PeerId } from '../peer-id/index.js' export interface Topology { - min?: number - max?: number - /** * If true, invoke `onConnect` for this topology on transient (e.g. short-lived * and/or data-limited) connections. (default: false) From 0e81e8b4f035f720039ab505699c1e9d4bb8b32a Mon Sep 17 00:00:00 2001 From: Cayman Date: Thu, 19 Oct 2023 15:32:35 -0400 Subject: [PATCH 4/9] refactor!: remove libp2p.keychain (#2084) Extract the keychain into it's own package. --- doc/migrations/v0.46-v1.0.0.md | 48 +- doc/migrations/v0.46-v1.0.md | 32 ++ packages/interface/src/index.ts | 15 - packages/interface/src/keychain/index.ts | 167 ------ packages/keychain/src/index.ts | 615 ++++------------------ packages/keychain/src/keychain.ts | 563 ++++++++++++++++++++ packages/keychain/test/keychain.spec.ts | 48 +- packages/libp2p/package.json | 1 - packages/libp2p/src/errors.ts | 1 - packages/libp2p/src/index.ts | 6 - packages/libp2p/src/libp2p.ts | 43 +- packages/libp2p/test/core/peer-id.spec.ts | 116 ---- 12 files changed, 769 insertions(+), 886 deletions(-) delete mode 100644 packages/interface/src/keychain/index.ts create mode 100644 packages/keychain/src/keychain.ts diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md index a8b22de3e5..b4bee48972 100644 --- a/doc/migrations/v0.46-v1.0.0.md +++ b/doc/migrations/v0.46-v1.0.0.md @@ -6,6 +6,8 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` - [New features](#new-features) - [Breaking changes](#breaking-changes) +- [KeyChain](#keychain) +- [Metrics](#metrics) ## New features @@ -13,4 +15,48 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` ## Breaking changes -... +```ts +import { autoNATService } from 'libp2p/autonat' +``` + +**After** + +```ts +import { autoNATService } from '@libp2p/autonat' +``` + +## KeyChain + +The KeyChain object is no longer included on Libp2p and must be instantiated explicitly if desired. + +**Before** + +```ts +import type { KeyChain } from '@libp2p/interface/keychain' + +const libp2p = await createLibp2p(...) + +const keychain: KeyChain = libp2p.keychain +``` + +**After** + +```ts +import { keychain, type Keychain } from '@libp2p/keychain' + +const libp2p = await createLibp2p({ + ... + services: { + keychain: keychain() + } +}) + +const keychain: Keychain = libp2p.services.keychain +``` + +## Metrics + +The following metrics were renamed: + +`libp2p_dialler_pending_dials` => `libp2p_dial_queue_pending_dials` +`libp2p_dialler_in_progress_dials` => `libp2p_dial_queue_in_progress_dials` diff --git a/doc/migrations/v0.46-v1.0.md b/doc/migrations/v0.46-v1.0.md index 184b266cf0..420048289a 100644 --- a/doc/migrations/v0.46-v1.0.md +++ b/doc/migrations/v0.46-v1.0.md @@ -32,6 +32,38 @@ __Describe__ --> +### KeyChain + +The KeyChain object is no longer included on Libp2p and must be instantiated explicitly if desired. + +**Before** + +```ts +import type { KeyChain } from '@libp2p/interface/keychain' + +const libp2p = await createLibp2p(...) + +const keychain: KeyChain = libp2p.keychain +``` + +***After*** + +```ts +import { DefaultKeyChain } from '@libp2p/keychain' +import type { KeyChain } from '@libp2p/interface/keychain' + +const libp2p = await createLibp2p({ + ... + services: { + keychain: (components) => new DefaultKeyChain(components, { + ...DefaultKeyChain.generateOptions() + }) + } +}) + +const keychain: KeyChain = libp2p.services.keychain +``` + ## Module Updates With this release you should update the following libp2p modules if you are relying on them: diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index 5b1463e4c2..c1f3b95a2c 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -17,7 +17,6 @@ import type { Connection, NewStreamOptions, Stream } from './connection/index.js' import type { ContentRouting } from './content-routing/index.js' import type { TypedEventTarget } from './events.js' -import type { KeyChain } from './keychain/index.js' import type { Metrics } from './metrics/index.js' import type { PeerId } from './peer-id/index.js' import type { PeerInfo } from './peer-info/index.js' @@ -377,20 +376,6 @@ export interface Libp2p extends Startable, Ty */ contentRouting: ContentRouting - /** - * The keychain contains the keys used by the current node, and can create new - * keys, export them, import them, etc. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('new key') - * console.info(keyInfo) - * // { id: '...', name: 'new key' } - * ``` - */ - keychain: KeyChain - /** * The metrics subsystem allows recording values to assess the health/performance * of the running node. diff --git a/packages/interface/src/keychain/index.ts b/packages/interface/src/keychain/index.ts deleted file mode 100644 index 0812ff1cd8..0000000000 --- a/packages/interface/src/keychain/index.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * @packageDocumentation - * - * The libp2p keychain provides an API to store keys in a datastore in - * an encrypted format. - * - * @example - * - * ```typescript - * import { createLibp2p } from 'libp2p' - * import { FsDatastore } from 'datastore-fs' - * - * const node = await createLibp2p({ - * datastore: new FsDatastore('/path/to/dir') - * }) - * - * const info = await node.keychain.createKey('my-new-key', 'Ed25519') - * - * console.info(info) // { id: '...', name: 'my-new-key' } - * ``` - */ - -import type { KeyType } from '../keys/index.js' -import type { PeerId } from '../peer-id/index.js' -import type { Multibase } from 'multiformats/bases/interface' - -export interface KeyInfo { - /** - * The universally unique key id - */ - id: string - - /** - * The local key name - */ - name: string -} - -export interface KeyChain { - /** - * Export an existing key as a PEM encrypted PKCS #8 string. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') - * ``` - */ - exportKey(name: string, password: string): Promise> - - /** - * Import a new key from a PEM encoded PKCS #8 string. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') - * const keyInfo = await libp2p.keychain.importKey('keyTestImport', pemKey, 'password123') - * ``` - */ - importKey(name: string, pem: string, password: string): Promise - - /** - * Import a new key from a PeerId with a private key component - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.importPeer('keyTestImport', peerIdFromString('12D3Foo...')) - * ``` - */ - importPeer(name: string, peerId: PeerId): Promise - - /** - * Export an existing key as a PeerId - * - * @example - * - * ```js - * const peerId = await libp2p.keychain.exportPeerId('key-name') - * ``` - */ - exportPeerId(name: string): Promise - - /** - * Create a key in the keychain. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * ``` - */ - createKey(name: string, type: KeyType, size?: number): Promise - - /** - * List all the keys. - * - * @example - * - * ```js - * const keyInfos = await libp2p.keychain.listKeys() - * ``` - */ - listKeys(): Promise - - /** - * Removes a key from the keychain. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo = await libp2p.keychain.removeKey('keyTest') - * ``` - */ - removeKey(name: string): Promise - - /** - * Rename a key in the keychain. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo = await libp2p.keychain.renameKey('keyTest', 'keyNewNtest') - * ``` - */ - renameKey(oldName: string, newName: string): Promise - - /** - * Find a key by it's id. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo2 = await libp2p.keychain.findKeyById(keyInfo.id) - * ``` - */ - findKeyById(id: string): Promise - - /** - * Find a key by it's name. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo2 = await libp2p.keychain.findKeyByName('keyTest') - * ``` - */ - findKeyByName(name: string): Promise - - /** - * Rotate keychain password and re-encrypt all associated keys - * - * @example - * - * ```js - * await libp2p.keychain.rotateKeychainPass('oldPassword', 'newPassword') - * ``` - */ - rotateKeychainPass(oldPass: string, newPass: string): Promise -} diff --git a/packages/keychain/src/index.ts b/packages/keychain/src/index.ts index 8507c4c7bf..848481bddc 100644 --- a/packages/keychain/src/index.ts +++ b/packages/keychain/src/index.ts @@ -50,25 +50,11 @@ * A key benefit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. */ -/* eslint max-nested-callbacks: ["error", 5] */ - -import { pbkdf2, randomBytes } from '@libp2p/crypto' -import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' -import { CodeError } from '@libp2p/interface/errors' -import { logger } from '@libp2p/logger' -import { peerIdFromKeys } from '@libp2p/peer-id' -import { Key } from 'interface-datastore/key' -import mergeOptions from 'merge-options' -import sanitize from 'sanitize-filename' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { codes } from './errors.js' -import type { KeyChain, KeyInfo } from '@libp2p/interface/keychain' +import { DefaultKeychain } from './keychain.js' import type { KeyType } from '@libp2p/interface/keys' import type { PeerId } from '@libp2p/interface/peer-id' import type { Datastore } from 'interface-datastore' - -const log = logger('libp2p:keychain') +import type { Multibase } from 'multiformats/bases/interface.js' export interface DEKConfig { hash: string @@ -77,556 +63,159 @@ export interface DEKConfig { keyLength: number } -export interface KeyChainInit { +export interface KeychainInit { pass?: string dek?: DEKConfig } -const keyPrefix = '/pkcs8/' -const infoPrefix = '/info/' -const privates = new WeakMap() - -// NIST SP 800-132 -const NIST = { - minKeyLength: 112 / 8, - minSaltLength: 128 / 8, - minIterationCount: 1000 -} - -const defaultOptions = { - // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ - dek: { - keyLength: 512 / 8, - iterationCount: 10000, - salt: 'you should override this value with a crypto secure random number', - hash: 'sha2-512' - } -} - -function validateKeyName (name: string): boolean { - if (name == null) { - return false - } - if (typeof name !== 'string') { - return false - } - return name === sanitize(name.trim()) && name.length > 0 -} - -/** - * Throws an error after a delay - * - * This assumes than an error indicates that the keychain is under attack. Delay returning an - * error to make brute force attacks harder. - */ -async function randomDelay (): Promise { - const min = 200 - const max = 1000 - const delay = Math.random() * (max - min) + min - - await new Promise(resolve => setTimeout(resolve, delay)) -} - -/** - * Converts a key name into a datastore name - */ -function DsName (name: string): Key { - return new Key(keyPrefix + name) -} - -/** - * Converts a key name into a datastore info name - */ -function DsInfoName (name: string): Key { - return new Key(infoPrefix + name) -} - -export interface KeyChainComponents { +export interface KeychainComponents { datastore: Datastore } -/** - * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. - * - * A key in the store has two entries - * - '/info/*key-name*', contains the KeyInfo for the key - * - '/pkcs8/*key-name*', contains the PKCS #8 for the key - * - */ -export class DefaultKeyChain implements KeyChain { - private readonly components: KeyChainComponents - private readonly init: KeyChainInit - +export interface KeyInfo { /** - * Creates a new instance of a key chain + * The universally unique key id */ - constructor (components: KeyChainComponents, init: KeyChainInit) { - this.components = components - this.init = mergeOptions(defaultOptions, init) - - // Enforce NIST SP 800-132 - if (this.init.pass != null && this.init.pass?.length < 20) { - throw new Error('pass must be least 20 characters') - } - if (this.init.dek?.keyLength != null && this.init.dek.keyLength < NIST.minKeyLength) { - throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) - } - if (this.init.dek?.salt?.length != null && this.init.dek.salt.length < NIST.minSaltLength) { - throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) - } - if (this.init.dek?.iterationCount != null && this.init.dek.iterationCount < NIST.minIterationCount) { - throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) - } - - const dek = this.init.pass != null && this.init.dek?.salt != null - ? pbkdf2( - this.init.pass, - this.init.dek?.salt, - this.init.dek?.iterationCount, - this.init.dek?.keyLength, - this.init.dek?.hash) - : '' - - privates.set(this, { dek }) - } + id: string /** - * Generates the options for a keychain. A random salt is produced. - * - * @returns {object} + * The local key name */ - static generateOptions (): KeyChainInit { - const options = Object.assign({}, defaultOptions) - const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding - options.dek.salt = uint8ArrayToString(randomBytes(saltLength), 'base64') - return options - } + name: string +} +export interface Keychain { /** - * Gets an object that can encrypt/decrypt protected data. - * The default options for a keychain. + * Export an existing key as a PEM encrypted PKCS #8 string. * - * @returns {object} - */ - static get options (): typeof defaultOptions { - return defaultOptions - } - - /** - * Create a new key. + * @example * - * @param {string} name - The local key name; cannot already exist. - * @param {string} type - One of the key types; 'rsa'. - * @param {number} [size = 2048] - The key size in bits. Used for rsa keys only + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') + * ``` */ - async createKey (name: string, type: KeyType, size = 2048): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) - } - - if (typeof type !== 'string') { - await randomDelay() - throw new CodeError('Invalid key type', codes.ERR_INVALID_KEY_TYPE) - } - - const dsname = DsName(name) - const exists = await this.components.datastore.has(dsname) - if (exists) { - await randomDelay() - throw new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) - } - - switch (type.toLowerCase()) { - case 'rsa': - if (!Number.isSafeInteger(size) || size < 2048) { - await randomDelay() - throw new CodeError('Invalid RSA key size', codes.ERR_INVALID_KEY_SIZE) - } - break - default: - break - } - - let keyInfo - try { - const keypair = await generateKeyPair(type, size) - const kid = await keypair.id() - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const pem = await keypair.export(dek) - keyInfo = { - name, - id: kid - } - const batch = this.components.datastore.batch() - batch.put(dsname, uint8ArrayFromString(pem)) - batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) - - await batch.commit() - } catch (err: any) { - await randomDelay() - throw err - } - - return keyInfo - } + exportKey(name: string, password: string): Promise> /** - * List all the keys. + * Import a new key from a PEM encoded PKCS #8 string. * - * @returns {Promise} - */ - async listKeys (): Promise { - const query = { - prefix: infoPrefix - } - - const info = [] - for await (const value of this.components.datastore.query(query)) { - info.push(JSON.parse(uint8ArrayToString(value.value))) - } - - return info - } - - /** - * Find a key by it's id + * @example + * + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') + * const keyInfo = await libp2p.keychain.importKey('keyTestImport', pemKey, 'password123') + * ``` */ - async findKeyById (id: string): Promise { - try { - const keys = await this.listKeys() - const key = keys.find((k) => k.id === id) - - if (key == null) { - throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) - } - - return key - } catch (err: any) { - await randomDelay() - throw err - } - } + importKey(name: string, pem: string, password: string): Promise /** - * Find a key by it's name. + * Import a new key from a PeerId with a private key component + * + * @example * - * @param {string} name - The local key name. - * @returns {Promise} + * ```js + * const keyInfo = await libp2p.keychain.importPeer('keyTestImport', peerIdFromString('12D3Foo...')) + * ``` */ - async findKeyByName (name: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - - const dsname = DsInfoName(name) - try { - const res = await this.components.datastore.get(dsname) - return JSON.parse(uint8ArrayToString(res)) - } catch (err: any) { - await randomDelay() - log.error(err) - throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) - } - } + importPeer(name: string, peerId: PeerId): Promise /** - * Remove an existing key. + * Export an existing key as a PeerId + * + * @example * - * @param {string} name - The local key name; must already exist. - * @returns {Promise} + * ```js + * const peerId = await libp2p.keychain.exportPeerId('key-name') + * ``` */ - async removeKey (name: string): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - const dsname = DsName(name) - const keyInfo = await this.findKeyByName(name) - const batch = this.components.datastore.batch() - batch.delete(dsname) - batch.delete(DsInfoName(name)) - await batch.commit() - return keyInfo - } + exportPeerId(name: string): Promise /** - * Rename a key + * Create a key in the keychain. + * + * @example * - * @param {string} oldName - The old local key name; must already exist. - * @param {string} newName - The new local key name; must not already exist. - * @returns {Promise} + * ```js + * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * ``` */ - async renameKey (oldName: string, newName: string): Promise { - if (!validateKeyName(oldName) || oldName === 'self') { - await randomDelay() - throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) - } - if (!validateKeyName(newName) || newName === 'self') { - await randomDelay() - throw new CodeError(`Invalid new key name '${newName}'`, codes.ERR_NEW_KEY_NAME_INVALID) - } - const oldDsname = DsName(oldName) - const newDsname = DsName(newName) - const oldInfoName = DsInfoName(oldName) - const newInfoName = DsInfoName(newName) - - const exists = await this.components.datastore.has(newDsname) - if (exists) { - await randomDelay() - throw new CodeError(`Key '${newName}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) - } - - try { - const pem = await this.components.datastore.get(oldDsname) - const res = await this.components.datastore.get(oldInfoName) - - const keyInfo = JSON.parse(uint8ArrayToString(res)) - keyInfo.name = newName - const batch = this.components.datastore.batch() - batch.put(newDsname, pem) - batch.put(newInfoName, uint8ArrayFromString(JSON.stringify(keyInfo))) - batch.delete(oldDsname) - batch.delete(oldInfoName) - await batch.commit() - return keyInfo - } catch (err: any) { - await randomDelay() - throw err - } - } + createKey(name: string, type: KeyType, size?: number): Promise /** - * Export an existing key as a PEM encrypted PKCS #8 string + * List all the keys. + * + * @example + * + * ```js + * const keyInfos = await libp2p.keychain.listKeys() + * ``` */ - async exportKey (name: string, password: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - if (password == null) { - await randomDelay() - throw new CodeError('Password is required', codes.ERR_PASSWORD_REQUIRED) - } - - const dsname = DsName(name) - try { - const res = await this.components.datastore.get(dsname) - const pem = uint8ArrayToString(res) - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const privateKey = await importKey(pem, dek) - const keyString = await privateKey.export(password) - - return keyString - } catch (err: any) { - await randomDelay() - throw err - } - } + listKeys(): Promise /** - * Export an existing key as a PeerId + * Removes a key from the keychain. + * + * @example + * + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo = await libp2p.keychain.removeKey('keyTest') + * ``` */ - async exportPeerId (name: string): Promise { - const password = 'temporary-password' - const pem = await this.exportKey(name, password) - const privateKey = await importKey(pem, password) - - return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) - } + removeKey(name: string): Promise /** - * Import a new key from a PEM encoded PKCS #8 string + * Rename a key in the keychain. * - * @param {string} name - The local key name; must not already exist. - * @param {string} pem - The PEM encoded PKCS #8 string - * @param {string} password - The password. - * @returns {Promise} + * @example + * + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo = await libp2p.keychain.renameKey('keyTest', 'keyNewNtest') + * ``` */ - async importKey (name: string, pem: string, password: string): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - if (pem == null) { - await randomDelay() - throw new CodeError('PEM encoded key is required', codes.ERR_PEM_REQUIRED) - } - const dsname = DsName(name) - const exists = await this.components.datastore.has(dsname) - if (exists) { - await randomDelay() - throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) - } - - let privateKey - try { - privateKey = await importKey(pem, password) - } catch (err: any) { - await randomDelay() - throw new CodeError('Cannot read the key, most likely the password is wrong', codes.ERR_CANNOT_READ_KEY) - } - - let kid - try { - kid = await privateKey.id() - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - pem = await privateKey.export(dek) - } catch (err: any) { - await randomDelay() - throw err - } - - const keyInfo = { - name, - id: kid - } - const batch = this.components.datastore.batch() - batch.put(dsname, uint8ArrayFromString(pem)) - batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) - await batch.commit() - - return keyInfo - } + renameKey(oldName: string, newName: string): Promise /** - * Import a peer key + * Find a key by it's id. + * + * @example + * + * ```js + * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo2 = await libp2p.keychain.findKeyById(keyInfo.id) + * ``` */ - async importPeer (name: string, peer: PeerId): Promise { - try { - if (!validateKeyName(name)) { - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - if (peer == null) { - throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) - } - if (peer.privateKey == null) { - throw new CodeError('PeerId.privKey is required', codes.ERR_MISSING_PRIVATE_KEY) - } - - const privateKey = await unmarshalPrivateKey(peer.privateKey) - - const dsname = DsName(name) - const exists = await this.components.datastore.has(dsname) - if (exists) { - await randomDelay() - throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) - } - - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const pem = await privateKey.export(dek) - const keyInfo: KeyInfo = { - name, - id: peer.toString() - } - const batch = this.components.datastore.batch() - batch.put(dsname, uint8ArrayFromString(pem)) - batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) - await batch.commit() - return keyInfo - } catch (err: any) { - await randomDelay() - throw err - } - } + findKeyById(id: string): Promise /** - * Gets the private key as PEM encoded PKCS #8 string + * Find a key by it's name. + * + * @example + * + * ```js + * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo2 = await libp2p.keychain.findKeyByName('keyTest') + * ``` */ - async getPrivateKey (name: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - - try { - const dsname = DsName(name) - const res = await this.components.datastore.get(dsname) - return uint8ArrayToString(res) - } catch (err: any) { - await randomDelay() - log.error(err) - throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) - } - } + findKeyByName(name: string): Promise /** * Rotate keychain password and re-encrypt all associated keys + * + * @example + * + * ```js + * await libp2p.keychain.rotateKeychainPass('oldPassword', 'newPassword') + * ``` */ - async rotateKeychainPass (oldPass: string, newPass: string): Promise { - if (typeof oldPass !== 'string') { - await randomDelay() - throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) - } - if (typeof newPass !== 'string') { - await randomDelay() - throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) - } - if (newPass.length < 20) { - await randomDelay() - throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) - } - log('recreating keychain') - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const oldDek = cached.dek - this.init.pass = newPass - const newDek = newPass != null && this.init.dek?.salt != null - ? pbkdf2( - newPass, - this.init.dek.salt, - this.init.dek?.iterationCount, - this.init.dek?.keyLength, - this.init.dek?.hash) - : '' - privates.set(this, { dek: newDek }) - const keys = await this.listKeys() - for (const key of keys) { - const res = await this.components.datastore.get(DsName(key.name)) - const pem = uint8ArrayToString(res) - const privateKey = await importKey(pem, oldDek) - const password = newDek.toString() - const keyAsPEM = await privateKey.export(password) + rotateKeychainPass(oldPass: string, newPass: string): Promise +} - // Update stored key - const batch = this.components.datastore.batch() - const keyInfo = { - name: key.name, - id: key.id - } - batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM)) - batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo))) - await batch.commit() - } - log('keychain reconstructed') +export function keychain (init: KeychainInit = {}): (components: KeychainComponents) => Keychain { + return (components: KeychainComponents) => { + return new DefaultKeychain(components, init) } } diff --git a/packages/keychain/src/keychain.ts b/packages/keychain/src/keychain.ts new file mode 100644 index 0000000000..1c64c06596 --- /dev/null +++ b/packages/keychain/src/keychain.ts @@ -0,0 +1,563 @@ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { pbkdf2, randomBytes } from '@libp2p/crypto' +import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { CodeError } from '@libp2p/interface/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { Key } from 'interface-datastore/key' +import mergeOptions from 'merge-options' +import sanitize from 'sanitize-filename' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { codes } from './errors.js' +import type { KeychainComponents, KeychainInit, Keychain, KeyInfo } from './index.js' +import type { KeyType } from '@libp2p/interface/keys' +import type { PeerId } from '@libp2p/interface/peer-id' + +const log = logger('libp2p:keychain') + +const keyPrefix = '/pkcs8/' +const infoPrefix = '/info/' +const privates = new WeakMap() + +// NIST SP 800-132 +const NIST = { + minKeyLength: 112 / 8, + minSaltLength: 128 / 8, + minIterationCount: 1000 +} + +const defaultOptions = { + // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + salt: 'you should override this value with a crypto secure random number', + hash: 'sha2-512' + } +} + +function validateKeyName (name: string): boolean { + if (name == null) { + return false + } + if (typeof name !== 'string') { + return false + } + return name === sanitize(name.trim()) && name.length > 0 +} + +/** + * Throws an error after a delay + * + * This assumes than an error indicates that the keychain is under attack. Delay returning an + * error to make brute force attacks harder. + */ +async function randomDelay (): Promise { + const min = 200 + const max = 1000 + const delay = Math.random() * (max - min) + min + + await new Promise(resolve => setTimeout(resolve, delay)) +} + +/** + * Converts a key name into a datastore name + */ +function DsName (name: string): Key { + return new Key(keyPrefix + name) +} + +/** + * Converts a key name into a datastore info name + */ +function DsInfoName (name: string): Key { + return new Key(infoPrefix + name) +} + +/** + * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. + * + * A key in the store has two entries + * - '/info/*key-name*', contains the KeyInfo for the key + * - '/pkcs8/*key-name*', contains the PKCS #8 for the key + * + */ +export class DefaultKeychain implements Keychain { + private readonly components: KeychainComponents + private readonly init: KeychainInit + + /** + * Creates a new instance of a key chain + */ + constructor (components: KeychainComponents, init: KeychainInit) { + this.components = components + this.init = mergeOptions(defaultOptions, init) + + // Enforce NIST SP 800-132 + if (this.init.pass != null && this.init.pass?.length < 20) { + throw new Error('pass must be least 20 characters') + } + if (this.init.dek?.keyLength != null && this.init.dek.keyLength < NIST.minKeyLength) { + throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) + } + if (this.init.dek?.salt?.length != null && this.init.dek.salt.length < NIST.minSaltLength) { + throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) + } + if (this.init.dek?.iterationCount != null && this.init.dek.iterationCount < NIST.minIterationCount) { + throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) + } + + const dek = this.init.pass != null && this.init.dek?.salt != null + ? pbkdf2( + this.init.pass, + this.init.dek?.salt, + this.init.dek?.iterationCount, + this.init.dek?.keyLength, + this.init.dek?.hash) + : '' + + privates.set(this, { dek }) + } + + /** + * Generates the options for a keychain. A random salt is produced. + * + * @returns {object} + */ + static generateOptions (): KeychainInit { + const options = Object.assign({}, defaultOptions) + const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding + options.dek.salt = uint8ArrayToString(randomBytes(saltLength), 'base64') + return options + } + + /** + * Gets an object that can encrypt/decrypt protected data. + * The default options for a keychain. + * + * @returns {object} + */ + static get options (): typeof defaultOptions { + return defaultOptions + } + + /** + * Create a new key. + * + * @param {string} name - The local key name; cannot already exist. + * @param {string} type - One of the key types; 'rsa'. + * @param {number} [size = 2048] - The key size in bits. Used for rsa keys only + */ + async createKey (name: string, type: KeyType, size = 2048): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) + } + + if (typeof type !== 'string') { + await randomDelay() + throw new CodeError('Invalid key type', codes.ERR_INVALID_KEY_TYPE) + } + + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) + } + + switch (type.toLowerCase()) { + case 'rsa': + if (!Number.isSafeInteger(size) || size < 2048) { + await randomDelay() + throw new CodeError('Invalid RSA key size', codes.ERR_INVALID_KEY_SIZE) + } + break + default: + break + } + + let keyInfo + try { + const keypair = await generateKeyPair(type, size) + const kid = await keypair.id() + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await keypair.export(dek) + keyInfo = { + name, + id: kid + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + + await batch.commit() + } catch (err: any) { + await randomDelay() + throw err + } + + return keyInfo + } + + /** + * List all the keys. + * + * @returns {Promise} + */ + async listKeys (): Promise { + const query = { + prefix: infoPrefix + } + + const info = [] + for await (const value of this.components.datastore.query(query)) { + info.push(JSON.parse(uint8ArrayToString(value.value))) + } + + return info + } + + /** + * Find a key by it's id + */ + async findKeyById (id: string): Promise { + try { + const keys = await this.listKeys() + const key = keys.find((k) => k.id === id) + + if (key == null) { + throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + + return key + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Find a key by it's name. + * + * @param {string} name - The local key name. + * @returns {Promise} + */ + async findKeyByName (name: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + + const dsname = DsInfoName(name) + try { + const res = await this.components.datastore.get(dsname) + return JSON.parse(uint8ArrayToString(res)) + } catch (err: any) { + await randomDelay() + log.error(err) + throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + } + + /** + * Remove an existing key. + * + * @param {string} name - The local key name; must already exist. + * @returns {Promise} + */ + async removeKey (name: string): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + const dsname = DsName(name) + const keyInfo = await this.findKeyByName(name) + const batch = this.components.datastore.batch() + batch.delete(dsname) + batch.delete(DsInfoName(name)) + await batch.commit() + return keyInfo + } + + /** + * Rename a key + * + * @param {string} oldName - The old local key name; must already exist. + * @param {string} newName - The new local key name; must not already exist. + * @returns {Promise} + */ + async renameKey (oldName: string, newName: string): Promise { + if (!validateKeyName(oldName) || oldName === 'self') { + await randomDelay() + throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) + } + if (!validateKeyName(newName) || newName === 'self') { + await randomDelay() + throw new CodeError(`Invalid new key name '${newName}'`, codes.ERR_NEW_KEY_NAME_INVALID) + } + const oldDsname = DsName(oldName) + const newDsname = DsName(newName) + const oldInfoName = DsInfoName(oldName) + const newInfoName = DsInfoName(newName) + + const exists = await this.components.datastore.has(newDsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${newName}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + try { + const pem = await this.components.datastore.get(oldDsname) + const res = await this.components.datastore.get(oldInfoName) + + const keyInfo = JSON.parse(uint8ArrayToString(res)) + keyInfo.name = newName + const batch = this.components.datastore.batch() + batch.put(newDsname, pem) + batch.put(newInfoName, uint8ArrayFromString(JSON.stringify(keyInfo))) + batch.delete(oldDsname) + batch.delete(oldInfoName) + await batch.commit() + return keyInfo + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PEM encrypted PKCS #8 string + */ + async exportKey (name: string, password: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (password == null) { + await randomDelay() + throw new CodeError('Password is required', codes.ERR_PASSWORD_REQUIRED) + } + + const dsname = DsName(name) + try { + const res = await this.components.datastore.get(dsname) + const pem = uint8ArrayToString(res) + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const privateKey = await importKey(pem, dek) + const keyString = await privateKey.export(password) + + return keyString + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PeerId + */ + async exportPeerId (name: string): Promise { + const password = 'temporary-password' + const pem = await this.exportKey(name, password) + const privateKey = await importKey(pem, password) + + return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) + } + + /** + * Import a new key from a PEM encoded PKCS #8 string + * + * @param {string} name - The local key name; must not already exist. + * @param {string} pem - The PEM encoded PKCS #8 string + * @param {string} password - The password. + * @returns {Promise} + */ + async importKey (name: string, pem: string, password: string): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (pem == null) { + await randomDelay() + throw new CodeError('PEM encoded key is required', codes.ERR_PEM_REQUIRED) + } + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + let privateKey + try { + privateKey = await importKey(pem, password) + } catch (err: any) { + await randomDelay() + throw new CodeError('Cannot read the key, most likely the password is wrong', codes.ERR_CANNOT_READ_KEY) + } + + let kid + try { + kid = await privateKey.id() + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + pem = await privateKey.export(dek) + } catch (err: any) { + await randomDelay() + throw err + } + + const keyInfo = { + name, + id: kid + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + + return keyInfo + } + + /** + * Import a peer key + */ + async importPeer (name: string, peer: PeerId): Promise { + try { + if (!validateKeyName(name)) { + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (peer == null) { + throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) + } + if (peer.privateKey == null) { + throw new CodeError('PeerId.privKey is required', codes.ERR_MISSING_PRIVATE_KEY) + } + + const privateKey = await unmarshalPrivateKey(peer.privateKey) + + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await privateKey.export(dek) + const keyInfo: KeyInfo = { + name, + id: peer.toString() + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + return keyInfo + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Gets the private key as PEM encoded PKCS #8 string + */ + async getPrivateKey (name: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + + try { + const dsname = DsName(name) + const res = await this.components.datastore.get(dsname) + return uint8ArrayToString(res) + } catch (err: any) { + await randomDelay() + log.error(err) + throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + } + + /** + * Rotate keychain password and re-encrypt all associated keys + */ + async rotateKeychainPass (oldPass: string, newPass: string): Promise { + if (typeof oldPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) + } + if (typeof newPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) + } + if (newPass.length < 20) { + await randomDelay() + throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) + } + log('recreating keychain') + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const oldDek = cached.dek + this.init.pass = newPass + const newDek = newPass != null && this.init.dek?.salt != null + ? pbkdf2( + newPass, + this.init.dek.salt, + this.init.dek?.iterationCount, + this.init.dek?.keyLength, + this.init.dek?.hash) + : '' + privates.set(this, { dek: newDek }) + const keys = await this.listKeys() + for (const key of keys) { + const res = await this.components.datastore.get(DsName(key.name)) + const pem = uint8ArrayToString(res) + const privateKey = await importKey(pem, oldDek) + const password = newDek.toString() + const keyAsPEM = await privateKey.export(password) + + // Update stored key + const batch = this.components.datastore.batch() + const keyInfo = { + name: key.name, + id: key.id + } + batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM)) + batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + } + log('keychain reconstructed') + } +} diff --git a/packages/keychain/test/keychain.spec.ts b/packages/keychain/test/keychain.spec.ts index 70a70bff53..89c1128485 100644 --- a/packages/keychain/test/keychain.spec.ts +++ b/packages/keychain/test/keychain.spec.ts @@ -9,8 +9,8 @@ import { MemoryDatastore } from 'datastore-core/memory' import { Key } from 'interface-datastore/key' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { DefaultKeyChain, type KeyChainInit } from '../src/index.js' -import type { KeyChain, KeyInfo } from '@libp2p/interface/keychain' +import { DefaultKeychain } from '../src/keychain.js' +import type { KeychainInit, Keychain, KeyInfo } from '../src/index.js' import type { PeerId } from '@libp2p/interface/peer-id' import type { Datastore } from 'interface-datastore' @@ -19,20 +19,20 @@ describe('keychain', () => { const rsaKeyName = 'tajné jméno' const renamedRsaKeyName = 'ชื่อลับ' let rsaKeyInfo: KeyInfo - let ks: DefaultKeyChain + let ks: DefaultKeychain let datastore2: Datastore before(async () => { datastore2 = new MemoryDatastore() - ks = new DefaultKeyChain({ + ks = new DefaultKeychain({ datastore: datastore2 }, { pass: passPhrase }) }) it('can start without a password', async () => { await expect(async function () { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: datastore2 }, {}) }()).to.eventually.be.ok() @@ -40,18 +40,18 @@ describe('keychain', () => { it('needs a NIST SP 800-132 non-weak pass phrase', async () => { await expect(async function () { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: datastore2 }, { pass: '< 20 character' }) }()).to.eventually.be.rejected() }) it('has default options', () => { - expect(DefaultKeyChain.options).to.exist() + expect(DefaultKeychain.options).to.exist() }) it('supports supported hashing alorithms', async () => { - const ok = new DefaultKeyChain({ + const ok = new DefaultKeychain({ datastore: datastore2 }, { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) expect(ok).to.exist() @@ -59,14 +59,14 @@ describe('keychain', () => { it('does not support unsupported hashing alorithms', async () => { await expect(async function () { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: datastore2 }, { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) }()).to.eventually.be.rejected() }) it('can list keys without a password', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: datastore2 }, {}) @@ -74,10 +74,10 @@ describe('keychain', () => { }) it('can find a key without a password', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: datastore2 }, {}) - const keychainWithPassword = new DefaultKeyChain({ + const keychainWithPassword = new DefaultKeychain({ datastore: datastore2 }, { pass: `hello-${Date.now()}-${Date.now()}` }) const name = `key-${Math.random()}` @@ -88,10 +88,10 @@ describe('keychain', () => { }) it('can remove a key without a password', async () => { - const keychainWithoutPassword = new DefaultKeyChain({ + const keychainWithoutPassword = new DefaultKeychain({ datastore: datastore2 }, {}) - const keychainWithPassword = new DefaultKeyChain({ + const keychainWithPassword = new DefaultKeychain({ datastore: datastore2 }, { pass: `hello-${Date.now()}-${Date.now()}` }) const name = `key-${Math.random()}` @@ -103,7 +103,7 @@ describe('keychain', () => { }) it('requires a name to create a password', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: datastore2 }, {}) @@ -112,9 +112,9 @@ describe('keychain', () => { }) it('can generate options', async () => { - const options = DefaultKeyChain.generateOptions() + const options = DefaultKeychain.generateOptions() options.pass = passPhrase - const chain = new DefaultKeyChain({ + const chain = new DefaultKeychain({ datastore: datastore2 }, options) expect(chain).to.exist() @@ -431,8 +431,8 @@ describe('keychain', () => { describe('rotate keychain passphrase', () => { let oldPass: string - let kc: KeyChain - let options: KeyChainInit + let kc: Keychain + let options: KeychainInit let ds: Datastore before(async () => { ds = new MemoryDatastore() @@ -446,7 +446,7 @@ describe('keychain', () => { hash: 'sha2-512' } } - kc = new DefaultKeyChain({ + kc = new DefaultKeychain({ datastore: ds }, options) }) @@ -509,7 +509,7 @@ describe('keychain', () => { describe('libp2p.keychain', () => { it('needs a passphrase to be used, otherwise throws an error', async () => { expect(() => { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: new MemoryDatastore() }, { pass: '' @@ -518,7 +518,7 @@ describe('libp2p.keychain', () => { }) it('can be used when a passphrase is provided', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: new MemoryDatastore() }, { pass: '12345678901234567890' @@ -530,7 +530,7 @@ describe('libp2p.keychain', () => { it('can reload keys', async () => { const datastore = new MemoryDatastore() - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore }, { pass: '12345678901234567890' @@ -539,7 +539,7 @@ describe('libp2p.keychain', () => { const kInfo = await keychain.createKey('keyName', 'Ed25519') expect(kInfo).to.exist() - const keychain2 = new DefaultKeyChain({ + const keychain2 = new DefaultKeychain({ datastore }, { pass: '12345678901234567890' diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index d2c6a5e1a7..d1b392ef5a 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -124,7 +124,6 @@ "@libp2p/crypto": "^2.0.7", "@libp2p/interface": "^0.1.5", "@libp2p/interface-internal": "^0.1.8", - "@libp2p/keychain": "^3.0.7", "@libp2p/logger": "^3.0.5", "@libp2p/multistream-select": "^4.0.5", "@libp2p/peer-collections": "^4.0.7", diff --git a/packages/libp2p/src/errors.ts b/packages/libp2p/src/errors.ts index a2d2ffb6c6..a895c1b5b2 100644 --- a/packages/libp2p/src/errors.ts +++ b/packages/libp2p/src/errors.ts @@ -48,7 +48,6 @@ export enum codes { ERR_NO_ROUTERS_AVAILABLE = 'ERR_NO_ROUTERS_AVAILABLE', ERR_CONNECTION_NOT_MULTIPLEXED = 'ERR_CONNECTION_NOT_MULTIPLEXED', ERR_NO_DIAL_TOKENS = 'ERR_NO_DIAL_TOKENS', - ERR_KEYCHAIN_REQUIRED = 'ERR_KEYCHAIN_REQUIRED', ERR_INVALID_CMS = 'ERR_INVALID_CMS', ERR_MISSING_KEYS = 'ERR_MISSING_KEYS', ERR_NO_KEY = 'ERR_NO_KEY', diff --git a/packages/libp2p/src/index.ts b/packages/libp2p/src/index.ts index 8fa5882135..b83ee40512 100644 --- a/packages/libp2p/src/index.ts +++ b/packages/libp2p/src/index.ts @@ -30,7 +30,6 @@ import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerRouting } from '@libp2p/interface/peer-routing' import type { StreamMuxerFactory } from '@libp2p/interface/stream-muxer' import type { Transport } from '@libp2p/interface/transport' -import type { KeyChainInit } from '@libp2p/keychain' import type { PersistentPeerStoreInit } from '@libp2p/peer-store' import type { Datastore } from 'interface-datastore' @@ -79,11 +78,6 @@ export interface Libp2pInit */ peerStore: PersistentPeerStoreInit - /** - * keychain configuration - */ - keychain: KeyChainInit - /** * An array that must include at least 1 compliant transport */ diff --git a/packages/libp2p/src/libp2p.ts b/packages/libp2p/src/libp2p.ts index 5708aa59e3..05cb801e2b 100644 --- a/packages/libp2p/src/libp2p.ts +++ b/packages/libp2p/src/libp2p.ts @@ -4,7 +4,6 @@ import { CodeError } from '@libp2p/interface/errors' import { TypedEventEmitter, CustomEvent, setMaxListeners } from '@libp2p/interface/events' import { peerDiscovery } from '@libp2p/interface/peer-discovery' import { type PeerRouting, peerRouting } from '@libp2p/interface/peer-routing' -import { DefaultKeyChain } from '@libp2p/keychain' import { logger } from '@libp2p/logger' import { PeerSet } from '@libp2p/peer-collections' import { peerIdFromString } from '@libp2p/peer-id' @@ -12,7 +11,6 @@ import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { PersistentPeerStore } from '@libp2p/peer-store' import { isMultiaddr, type Multiaddr } from '@multiformats/multiaddr' import { MemoryDatastore } from 'datastore-core/memory' -import mergeOptions from 'merge-options' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { DefaultAddressManager } from './address-manager/index.js' @@ -30,14 +28,12 @@ import type { Components } from './components.js' import type { Libp2p, Libp2pInit, Libp2pOptions } from './index.js' import type { Libp2pEvents, PendingDial, ServiceMap, AbortOptions } from '@libp2p/interface' import type { Connection, NewStreamOptions, Stream } from '@libp2p/interface/connection' -import type { KeyChain } from '@libp2p/interface/keychain' import type { Metrics } from '@libp2p/interface/metrics' import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { PeerStore } from '@libp2p/interface/peer-store' import type { Topology } from '@libp2p/interface/topology' import type { StreamHandler, StreamHandlerOptions } from '@libp2p/interface-internal/registrar' -import type { Datastore } from 'interface-datastore' const log = logger('libp2p') @@ -46,7 +42,6 @@ export class Libp2pNode> extends public peerStore: PeerStore public contentRouting: ContentRouting public peerRouting: PeerRouting - public keychain: KeyChain public metrics?: Metrics public services: T @@ -130,13 +125,6 @@ export class Libp2pNode> extends // Addresses {listen, announce, noAnnounce} this.configureComponent('addressManager', new DefaultAddressManager(this.components, init.addresses)) - // Create keychain - const keychainOpts = DefaultKeyChain.generateOptions() - this.keychain = this.configureComponent('keyChain', new DefaultKeyChain(this.components, { - ...keychainOpts, - ...init.keychain - })) - // Peer routers const peerRouters: PeerRouting[] = (init.peerRouters ?? []).map((fn, index) => this.configureComponent(`peer-router-${index}`, fn(this.components))) this.peerRouting = this.components.peerRouting = this.configureComponent('peerRouting', new DefaultPeerRouting(this.components, { @@ -219,13 +207,6 @@ export class Libp2pNode> extends log('libp2p is starting') - const keys = await this.keychain.listKeys() - - if (keys.find(key => key.name === 'self') == null) { - log('importing self key into keychain') - await this.keychain.importPeer('self', this.components.peerId) - } - try { await this.components.beforeStart?.() await this.components.start() @@ -408,29 +389,7 @@ export class Libp2pNode> extends * libp2p interface and is useful for testing and debugging. */ export async function createLibp2pNode > (options: Libp2pOptions): Promise> { - if (options.peerId == null) { - const datastore = options.datastore as Datastore | undefined - - if (datastore != null) { - try { - // try load the peer id from the keychain - const keyChain = new DefaultKeyChain({ - datastore - }, mergeOptions(DefaultKeyChain.generateOptions(), options.keychain)) - - options.peerId = await keyChain.exportPeerId('self') - } catch (err: any) { - if (err.code !== 'ERR_NOT_FOUND') { - throw err - } - } - } - } - - if (options.peerId == null) { - // no peer id in the keychain, create a new peer id - options.peerId = await createEd25519PeerId() - } + options.peerId ??= await createEd25519PeerId() return new Libp2pNode(validateConfig(options)) } diff --git a/packages/libp2p/test/core/peer-id.spec.ts b/packages/libp2p/test/core/peer-id.spec.ts index 663610c6a4..220a9fbe16 100644 --- a/packages/libp2p/test/core/peer-id.spec.ts +++ b/packages/libp2p/test/core/peer-id.spec.ts @@ -2,7 +2,6 @@ import { webSockets } from '@libp2p/websockets' import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core' import { createLibp2p, type Libp2p } from '../../src/index.js' import { plaintext } from '../../src/insecure/index.js' @@ -27,119 +26,4 @@ describe('peer-id', () => { expect(libp2p.peerId).to.be.ok() }) - - it('should retrieve the PeerId from the datastore', async () => { - const datastore = new MemoryDatastore() - - libp2p = await createLibp2p({ - datastore, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // this PeerId was created by default - const peerId = libp2p.peerId - - await libp2p.stop() - - // create a new node from the same datastore - libp2p = await createLibp2p({ - datastore, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // the new node should have read the PeerId from the datastore - // instead of creating a new one - expect(libp2p.peerId.toString()).to.equal(peerId.toString()) - }) - - it('should retrieve the PeerId from the datastore with a keychain password', async () => { - const datastore = new MemoryDatastore() - const keychain = { - pass: 'very-long-password-must-be-over-twenty-characters-long', - dek: { - salt: 'CpjNIxMqAZ+aJg+ezLfuzG4a' - } - } - - libp2p = await createLibp2p({ - datastore, - keychain, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // this PeerId was created by default - const peerId = libp2p.peerId - - await libp2p.stop() - - // create a new node from the same datastore - libp2p = await createLibp2p({ - datastore, - keychain, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // the new node should have read the PeerId from the datastore - // instead of creating a new one - expect(libp2p.peerId.toString()).to.equal(peerId.toString()) - }) - - it('should fail to start if retrieving the PeerId from the datastore fails', async () => { - const datastore = new MemoryDatastore() - const keychain = { - pass: 'very-long-password-must-be-over-twenty-characters-long', - dek: { - salt: 'CpjNIxMqAZ+aJg+ezLfuzG4a' - } - } - - libp2p = await createLibp2p({ - datastore, - keychain, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - await libp2p.stop() - - // creating a new node from the same datastore but with the wrong keychain config should fail - await expect(createLibp2p({ - datastore, - keychain: { - pass: 'different-very-long-password-must-be-over-twenty-characters-long', - dek: { - salt: 'different-CpjNIxMqAZ+aJg+ezLfuzG4a' - } - }, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - })).to.eventually.rejectedWith('Invalid PEM formatted message') - }) }) From 41629a69cf9b69027c5df3e8038a0a22bdba2f16 Mon Sep 17 00:00:00 2001 From: Cayman Date: Mon, 23 Oct 2023 17:50:22 -0400 Subject: [PATCH 5/9] refactor!: move autonat into separate package (#2107) Co-authored-by: chad --- doc/CONFIGURATION.md | 2 +- doc/migrations/v0.46-v1.0.0.md | 10 +-- doc/migrations/v0.46-v1.0.md | 85 ------------------- packages/interface/src/errors.ts | 4 + packages/libp2p/package.json | 6 -- .../src/connection-manager/dial-queue.ts | 4 +- packages/libp2p/src/content-routing/index.ts | 2 +- packages/libp2p/src/errors.ts | 1 - packages/libp2p/src/fetch/index.ts | 4 +- packages/libp2p/src/ping/index.ts | 4 +- packages/libp2p/src/upgrader.ts | 4 +- .../test/connection-manager/direct.node.ts | 4 +- .../test/connection-manager/direct.spec.ts | 4 +- packages/libp2p/test/fetch/index.spec.ts | 3 +- packages/libp2p/test/ping/index.spec.ts | 3 +- packages/libp2p/typedoc.json | 1 - packages/protocol-autonat/LICENSE | 4 + packages/protocol-autonat/LICENSE-APACHE | 5 ++ packages/protocol-autonat/LICENSE-MIT | 19 +++++ packages/protocol-autonat/README.md | 64 ++++++++++++++ packages/protocol-autonat/package.json | 71 ++++++++++++++++ .../src}/constants.ts | 0 .../autonat => protocol-autonat/src}/index.ts | 9 +- .../src}/pb/index.proto | 0 .../src}/pb/index.ts | 0 .../test}/index.spec.ts | 28 +++--- packages/protocol-autonat/tsconfig.json | 27 ++++++ packages/protocol-autonat/typedoc.json | 5 ++ 28 files changed, 237 insertions(+), 136 deletions(-) delete mode 100644 doc/migrations/v0.46-v1.0.md create mode 100644 packages/protocol-autonat/LICENSE create mode 100644 packages/protocol-autonat/LICENSE-APACHE create mode 100644 packages/protocol-autonat/LICENSE-MIT create mode 100644 packages/protocol-autonat/README.md create mode 100644 packages/protocol-autonat/package.json rename packages/{libp2p/src/autonat => protocol-autonat/src}/constants.ts (100%) rename packages/{libp2p/src/autonat => protocol-autonat/src}/index.ts (98%) rename packages/{libp2p/src/autonat => protocol-autonat/src}/pb/index.proto (100%) rename packages/{libp2p/src/autonat => protocol-autonat/src}/pb/index.ts (100%) rename packages/{libp2p/test/autonat => protocol-autonat/test}/index.spec.ts (96%) create mode 100644 packages/protocol-autonat/tsconfig.json create mode 100644 packages/protocol-autonat/typedoc.json diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 2173ac59be..6f82e087bd 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -930,7 +930,7 @@ For more information see https://docs.libp2p.io/concepts/nat/autonat/#what-is-au ```ts import { createLibp2p } from 'libp2p' -import { autoNATService } from 'libp2p/autonat' +import { autoNATService } from '@libp2p/autonat' const node = await createLibp2p({ services: { diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md index b4bee48972..82132e798f 100644 --- a/doc/migrations/v0.46-v1.0.0.md +++ b/doc/migrations/v0.46-v1.0.0.md @@ -1,19 +1,19 @@ + # Migrating to libp2p@1.0.0 A migration guide for refactoring your application code from libp2p `v0.46` to `v1.0.0`. ## Table of Contents -- [New features](#new-features) -- [Breaking changes](#breaking-changes) +- [AutoNAT](#autonat) - [KeyChain](#keychain) - [Metrics](#metrics) -## New features +## AutoNAT -... +The AutoNAT service is now published in its own package. -## Breaking changes +**Before** ```ts import { autoNATService } from 'libp2p/autonat' diff --git a/doc/migrations/v0.46-v1.0.md b/doc/migrations/v0.46-v1.0.md deleted file mode 100644 index 420048289a..0000000000 --- a/doc/migrations/v0.46-v1.0.md +++ /dev/null @@ -1,85 +0,0 @@ - -# Migrating to libp2p@1.0 - -A migration guide for refactoring your application code from libp2p v0.46 to v1.0. - -## Table of Contents - -- [API](#api) -- [Module Updates](#module-updates) -- [Metrics](#metrics) - -## API - - - -### KeyChain - -The KeyChain object is no longer included on Libp2p and must be instantiated explicitly if desired. - -**Before** - -```ts -import type { KeyChain } from '@libp2p/interface/keychain' - -const libp2p = await createLibp2p(...) - -const keychain: KeyChain = libp2p.keychain -``` - -***After*** - -```ts -import { DefaultKeyChain } from '@libp2p/keychain' -import type { KeyChain } from '@libp2p/interface/keychain' - -const libp2p = await createLibp2p({ - ... - services: { - keychain: (components) => new DefaultKeyChain(components, { - ...DefaultKeyChain.generateOptions() - }) - } -}) - -const keychain: KeyChain = libp2p.services.keychain -``` - -## Module Updates - -With this release you should update the following libp2p modules if you are relying on them: - - - -```json - -``` - -## Metrics - -The following metrics were renamed: - -`libp2p_dialler_pending_dials` => `libp2p_dial_queue_pending_dials` -`libp2p_dialler_in_progress_dials` => `libp2p_dial_queue_in_progress_dials` diff --git a/packages/interface/src/errors.ts b/packages/interface/src/errors.ts index a5911adb76..4f5f8b4c3a 100644 --- a/packages/interface/src/errors.ts +++ b/packages/interface/src/errors.ts @@ -65,3 +65,7 @@ export class InvalidCryptoTransmissionError extends Error { static readonly code = 'ERR_INVALID_CRYPTO_TRANSMISSION' } + +// Error codes + +export const ERR_TIMEOUT = 'ERR_TIMEOUT' diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index d1b392ef5a..efdd79672f 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -48,10 +48,6 @@ "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" }, - "./autonat": { - "types": "./dist/src/autonat/index.d.ts", - "import": "./dist/src/autonat/index.js" - }, "./circuit-relay": { "types": "./dist/src/circuit-relay/index.d.ts", "import": "./dist/src/circuit-relay/index.js" @@ -104,7 +100,6 @@ "prepublishOnly": "node scripts/update-version.js && npm run build", "build": "aegir build", "generate": "run-s generate:proto:*", - "generate:proto:autonat": "protons ./src/autonat/pb/index.proto", "generate:proto:circuit-relay": "protons ./src/circuit-relay/pb/index.proto", "generate:proto:dcutr": "protons ./src/dcutr/pb/message.proto", "generate:proto:fetch": "protons ./src/fetch/pb/proto.proto", @@ -148,7 +143,6 @@ "it-map": "^3.0.3", "it-merge": "^3.0.0", "it-pair": "^2.0.6", - "it-parallel": "^3.0.0", "it-pipe": "^3.0.1", "it-protobuf-stream": "^1.0.0", "it-stream-types": "^2.0.1", diff --git a/packages/libp2p/src/connection-manager/dial-queue.ts b/packages/libp2p/src/connection-manager/dial-queue.ts index a724796406..a32c38102f 100644 --- a/packages/libp2p/src/connection-manager/dial-queue.ts +++ b/packages/libp2p/src/connection-manager/dial-queue.ts @@ -1,4 +1,4 @@ -import { AbortError, CodeError } from '@libp2p/interface/errors' +import { AbortError, CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import { PeerMap } from '@libp2p/peer-collections' @@ -269,7 +269,7 @@ export class DialQueue { // Error is a timeout if (signal.aborted) { - const error = new CodeError(err.message, codes.ERR_TIMEOUT) + const error = new CodeError(err.message, ERR_TIMEOUT) throw error } diff --git a/packages/libp2p/src/content-routing/index.ts b/packages/libp2p/src/content-routing/index.ts index f8a17a3cdd..a96c2bd9ab 100644 --- a/packages/libp2p/src/content-routing/index.ts +++ b/packages/libp2p/src/content-routing/index.ts @@ -1,7 +1,7 @@ import { CodeError } from '@libp2p/interface/errors' import merge from 'it-merge' import { pipe } from 'it-pipe' -import { messages, codes } from '../errors.js' +import { codes, messages } from '../errors.js' import { storeAddresses, uniquePeers, diff --git a/packages/libp2p/src/errors.ts b/packages/libp2p/src/errors.ts index a895c1b5b2..0b84215aa9 100644 --- a/packages/libp2p/src/errors.ts +++ b/packages/libp2p/src/errors.ts @@ -37,7 +37,6 @@ export enum codes { ERR_INVALID_PEER = 'ERR_INVALID_PEER', ERR_MUXER_UNAVAILABLE = 'ERR_MUXER_UNAVAILABLE', ERR_NOT_FOUND = 'ERR_NOT_FOUND', - ERR_TIMEOUT = 'ERR_TIMEOUT', ERR_TRANSPORT_UNAVAILABLE = 'ERR_TRANSPORT_UNAVAILABLE', ERR_TRANSPORT_DIAL_FAILED = 'ERR_TRANSPORT_DIAL_FAILED', ERR_UNSUPPORTED_PROTOCOL = 'ERR_UNSUPPORTED_PROTOCOL', diff --git a/packages/libp2p/src/fetch/index.ts b/packages/libp2p/src/fetch/index.ts index 6e345f0d58..b9fad11e92 100644 --- a/packages/libp2p/src/fetch/index.ts +++ b/packages/libp2p/src/fetch/index.ts @@ -1,4 +1,4 @@ -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import first from 'it-first' @@ -155,7 +155,7 @@ class DefaultFetchService implements Startable, FetchService { }) onAbort = () => { - stream?.abort(new CodeError('fetch timeout', codes.ERR_TIMEOUT)) + stream?.abort(new CodeError('fetch timeout', ERR_TIMEOUT)) } // make stream abortable diff --git a/packages/libp2p/src/ping/index.ts b/packages/libp2p/src/ping/index.ts index ea8d23db5e..ea87229d9a 100644 --- a/packages/libp2p/src/ping/index.ts +++ b/packages/libp2p/src/ping/index.ts @@ -1,5 +1,5 @@ import { randomBytes } from '@libp2p/crypto' -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' import first from 'it-first' import { pipe } from 'it-pipe' @@ -118,7 +118,7 @@ class DefaultPingService implements Startable, PingService { }) onAbort = () => { - stream?.abort(new CodeError('ping timeout', codes.ERR_TIMEOUT)) + stream?.abort(new CodeError('ping timeout', ERR_TIMEOUT)) } // make stream abortable diff --git a/packages/libp2p/src/upgrader.ts b/packages/libp2p/src/upgrader.ts index 4000cde7a8..2b168def22 100644 --- a/packages/libp2p/src/upgrader.ts +++ b/packages/libp2p/src/upgrader.ts @@ -1,4 +1,4 @@ -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import * as mss from '@libp2p/multistream-select' @@ -165,7 +165,7 @@ export class DefaultUpgrader implements Upgrader { const signal = AbortSignal.timeout(this.inboundUpgradeTimeout) const onAbort = (): void => { - maConn.abort(new CodeError('inbound upgrade timeout', codes.ERR_TIMEOUT)) + maConn.abort(new CodeError('inbound upgrade timeout', ERR_TIMEOUT)) } signal.addEventListener('abort', onAbort, { once: true }) diff --git a/packages/libp2p/test/connection-manager/direct.node.ts b/packages/libp2p/test/connection-manager/direct.node.ts index 72f478d332..4b225596da 100644 --- a/packages/libp2p/test/connection-manager/direct.node.ts +++ b/packages/libp2p/test/connection-manager/direct.node.ts @@ -5,7 +5,7 @@ import os from 'node:os' import path from 'node:path' import { yamux } from '@chainsafe/libp2p-yamux' import { type Connection, type ConnectionProtector, isConnection } from '@libp2p/interface/connection' -import { AbortError } from '@libp2p/interface/errors' +import { AbortError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' import { mockConnection, mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' @@ -218,7 +218,7 @@ describe('dialing (direct, TCP)', () => { await expect(dialer.dial(remoteAddr)) .to.eventually.be.rejectedWith(Error) - .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) + .and.to.have.property('code', ERR_TIMEOUT) }) it('should dial to the max concurrency', async () => { diff --git a/packages/libp2p/test/connection-manager/direct.spec.ts b/packages/libp2p/test/connection-manager/direct.spec.ts index 305ae4e524..7f9d9d1ab4 100644 --- a/packages/libp2p/test/connection-manager/direct.spec.ts +++ b/packages/libp2p/test/connection-manager/direct.spec.ts @@ -1,7 +1,7 @@ /* eslint-env mocha */ import { yamux } from '@chainsafe/libp2p-yamux' -import { AbortError } from '@libp2p/interface/errors' +import { AbortError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader, mockConnection } from '@libp2p/interface-compliance-tests/mocks' import { mplex } from '@libp2p/mplex' @@ -167,7 +167,7 @@ describe('dialing (direct, WebSockets)', () => { await expect(connectionManager.openConnection(remoteAddr)) .to.eventually.be.rejected() - .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) + .and.to.have.property('code', ERR_TIMEOUT) }) it('should throw when a peer advertises more than the allowed number of addresses', async () => { diff --git a/packages/libp2p/test/fetch/index.spec.ts b/packages/libp2p/test/fetch/index.spec.ts index 7b90a0f7db..dd7d1d921a 100644 --- a/packages/libp2p/test/fetch/index.spec.ts +++ b/packages/libp2p/test/fetch/index.spec.ts @@ -1,5 +1,6 @@ /* eslint-env mocha */ +import { ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' import { mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks' @@ -135,7 +136,7 @@ describe('fetch', () => { await expect(localFetch.fetch(remoteComponents.peerId, key, { signal })) - .to.eventually.be.rejected.with.property('code', 'ERR_TIMEOUT') + .to.eventually.be.rejected.with.property('code', ERR_TIMEOUT) // should have closed stream expect(newStreamSpy).to.have.property('callCount', 1) diff --git a/packages/libp2p/test/ping/index.spec.ts b/packages/libp2p/test/ping/index.spec.ts index 9b6aef1385..dd6c877f50 100644 --- a/packages/libp2p/test/ping/index.spec.ts +++ b/packages/libp2p/test/ping/index.spec.ts @@ -1,5 +1,6 @@ /* eslint-env mocha */ +import { ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' import { mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks' @@ -125,7 +126,7 @@ describe('ping', () => { await expect(localPing.ping(remoteComponents.peerId, { signal })) - .to.eventually.be.rejected.with.property('code', 'ERR_TIMEOUT') + .to.eventually.be.rejected.with.property('code', ERR_TIMEOUT) // should have closed stream expect(newStreamSpy).to.have.property('callCount', 1) diff --git a/packages/libp2p/typedoc.json b/packages/libp2p/typedoc.json index 8ed11ea9f3..5629b3c79d 100644 --- a/packages/libp2p/typedoc.json +++ b/packages/libp2p/typedoc.json @@ -1,7 +1,6 @@ { "entryPoints": [ "./src/index.ts", - "./src/autonat/index.ts", "./src/circuit-relay/index.ts", "./src/fetch/index.ts", "./src/identify/index.ts", diff --git a/packages/protocol-autonat/LICENSE b/packages/protocol-autonat/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/protocol-autonat/LICENSE @@ -0,0 +1,4 @@ +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/packages/protocol-autonat/LICENSE-APACHE b/packages/protocol-autonat/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/protocol-autonat/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/packages/protocol-autonat/LICENSE-MIT b/packages/protocol-autonat/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/protocol-autonat/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/packages/protocol-autonat/README.md b/packages/protocol-autonat/README.md new file mode 100644 index 0000000000..e11bd46ab6 --- /dev/null +++ b/packages/protocol-autonat/README.md @@ -0,0 +1,64 @@ +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of Autonat Protocol + +# About + +Use the `autoNATService` function to add support for the [AutoNAT protocol](https://docs.libp2p.io/concepts/nat/autonat/) +to libp2p. + +## Example + +```typescript +import { createLibp2p } from 'libp2p' +import { autoNATService } from '@libp2p/autonat' + +const node = await createLibp2p({ + // ...other options + services: { + autoNAT: autoNATService() + } +}) +``` + +## Table of contents + +- [About](#about) + - [Example](#example) +- [Install](#install) + - [Browser ` +``` + +# API Docs + +- + +# License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-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/packages/protocol-autonat/package.json b/packages/protocol-autonat/package.json new file mode 100644 index 0000000000..0648c94c11 --- /dev/null +++ b/packages/protocol-autonat/package.json @@ -0,0 +1,71 @@ +{ + "name": "@libp2p/autonat", + "version": "1.0.0", + "description": "Implementation of Autonat Protocol", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/protocol-autonat#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "start": "node dist/src/main.js", + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "generate": "protons ./src/pb/index.proto", + "lint": "aegir lint", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/interface": "^0.1.2", + "@libp2p/interface-internal": "^0.1.5", + "@libp2p/peer-id": "^3.0.2" , + "@libp2p/peer-id-factory": "^3.0.4" , + "@libp2p/logger": "^3.0.2", + "@multiformats/multiaddr": "^12.1.5", + "it-first": "^3.0.1", + "it-length-prefixed": "^9.0.1", + "it-map": "^3.0.3", + "it-parallel": "^3.0.0", + "it-pipe": "^3.0.1", + "private-ip": "^3.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3" + }, + "devDependencies": { + "aegir": "^41.0.2", + "it-all": "^3.0.1", + "it-pushable": "^3.2.0", + "sinon": "^17.0.0", + "sinon-ts": "^1.0.0" + } +} diff --git a/packages/libp2p/src/autonat/constants.ts b/packages/protocol-autonat/src/constants.ts similarity index 100% rename from packages/libp2p/src/autonat/constants.ts rename to packages/protocol-autonat/src/constants.ts diff --git a/packages/libp2p/src/autonat/index.ts b/packages/protocol-autonat/src/index.ts similarity index 98% rename from packages/libp2p/src/autonat/index.ts rename to packages/protocol-autonat/src/index.ts index f958d76621..3ca49d43e6 100644 --- a/packages/libp2p/src/autonat/index.ts +++ b/packages/protocol-autonat/src/index.ts @@ -8,7 +8,7 @@ * * ```typescript * import { createLibp2p } from 'libp2p' - * import { autoNATService } from 'libp2p/autonat' + * import { autoNATService } from '@libp2p/autonat' * * const node = await createLibp2p({ * // ...other options @@ -19,7 +19,7 @@ * ``` */ -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import { peerIdFromBytes } from '@libp2p/peer-id' @@ -31,7 +31,6 @@ import map from 'it-map' import parallel from 'it-parallel' import { pipe } from 'it-pipe' import isPrivateIp from 'private-ip' -import { codes } from '../errors.js' import { MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS, @@ -157,7 +156,7 @@ class DefaultAutoNATService implements Startable { const signal = AbortSignal.timeout(this.timeout) const onAbort = (): void => { - data.stream.abort(new CodeError('handleIncomingAutonatStream timeout', codes.ERR_TIMEOUT)) + data.stream.abort(new CodeError('handleIncomingAutonatStream timeout', ERR_TIMEOUT)) } signal.addEventListener('abort', onAbort, { once: true }) @@ -466,7 +465,7 @@ class DefaultAutoNATService implements Startable { signal }) - onAbort = () => { stream.abort(new CodeError('verifyAddress timeout', codes.ERR_TIMEOUT)) } + onAbort = () => { stream.abort(new CodeError('verifyAddress timeout', ERR_TIMEOUT)) } signal.addEventListener('abort', onAbort, { once: true }) diff --git a/packages/libp2p/src/autonat/pb/index.proto b/packages/protocol-autonat/src/pb/index.proto similarity index 100% rename from packages/libp2p/src/autonat/pb/index.proto rename to packages/protocol-autonat/src/pb/index.proto diff --git a/packages/libp2p/src/autonat/pb/index.ts b/packages/protocol-autonat/src/pb/index.ts similarity index 100% rename from packages/libp2p/src/autonat/pb/index.ts rename to packages/protocol-autonat/src/pb/index.ts diff --git a/packages/libp2p/test/autonat/index.spec.ts b/packages/protocol-autonat/test/index.spec.ts similarity index 96% rename from packages/libp2p/test/autonat/index.spec.ts rename to packages/protocol-autonat/test/index.spec.ts index 250033f7b3..09beda2c67 100644 --- a/packages/libp2p/test/autonat/index.spec.ts +++ b/packages/protocol-autonat/test/index.spec.ts @@ -12,20 +12,17 @@ import { pushable } from 'it-pushable' import sinon from 'sinon' import { stubInterface } from 'sinon-ts' import { Uint8ArrayList } from 'uint8arraylist' -import { PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION } from '../../src/autonat/constants.js' -import { autoNATService } from '../../src/autonat/index.js' -import { Message } from '../../src/autonat/pb/index.js' -import { defaultComponents } from '../../src/components.js' -import type { AutoNATServiceInit } from '../../src/autonat/index.js' -import type { Components } from '../../src/components.js' -import type { DefaultConnectionManager } from '../../src/connection-manager/index.js' +import { PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION } from '../src/constants.js' +import { autoNATService } from '../src/index.js' +import { Message } from '../src/pb/index.js' +import type { AutoNATComponents, AutoNATServiceInit } from '../src/index.js' import type { Connection, Stream } from '@libp2p/interface/connection' import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { PeerRouting } from '@libp2p/interface/peer-routing' -import type { PeerStore } from '@libp2p/interface/peer-store' import type { Transport } from '@libp2p/interface/transport' import type { AddressManager } from '@libp2p/interface-internal/address-manager' +import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' import type { Registrar } from '@libp2p/interface-internal/registrar' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' import type { Multiaddr } from '@multiformats/multiaddr' @@ -42,13 +39,12 @@ const defaultInit: AutoNATServiceInit = { describe('autonat', () => { let service: any - let components: Components + let components: AutoNATComponents let peerRouting: StubbedInstance let registrar: StubbedInstance let addressManager: StubbedInstance - let connectionManager: StubbedInstance + let connectionManager: StubbedInstance let transportManager: StubbedInstance - let peerStore: StubbedInstance beforeEach(async () => { peerRouting = stubInterface() @@ -56,19 +52,17 @@ describe('autonat', () => { addressManager = stubInterface() addressManager.getAddresses.returns([]) - connectionManager = stubInterface() + connectionManager = stubInterface() transportManager = stubInterface() - peerStore = stubInterface() - components = defaultComponents({ + components = { peerId: await createEd25519PeerId(), peerRouting, registrar, addressManager, connectionManager, - transportManager, - peerStore - }) + transportManager + } service = autoNATService(defaultInit)(components) diff --git a/packages/protocol-autonat/tsconfig.json b/packages/protocol-autonat/tsconfig.json new file mode 100644 index 0000000000..fd38c2c239 --- /dev/null +++ b/packages/protocol-autonat/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + }, + { + "path": "../interface-internal" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../logger" + } + ] +} diff --git a/packages/protocol-autonat/typedoc.json b/packages/protocol-autonat/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/protocol-autonat/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +} From 9f2ed20900d7184b4cad29240c5a98af2bc7cff6 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 24 Oct 2023 18:55:05 +0100 Subject: [PATCH 6/9] fix!: remove protocols from PeerInfo (#2166) A [PeerInfo](https://docs.libp2p.io/concepts/fundamentals/peers/#peer-info) is a libp2p object that combines a PeerID and some Multiaddrs. We also add a list of protocols. This was a mistake because protocols are exchanged during Identify but the PeerInfo object is used for peer discovery. This is evident because we set the protocol list to an empty array everywhere. PeerInfo is useful for exchanging peer data with other nodes, if we need a more fleshed-out peer representation we'd use the `Peer` interface from the peer store. BREAKING CHANGE: the `.protocols` property has been removed from the `PeerInfo` interface --- .../src/mocks/peer-discovery.ts | 3 +-- packages/interface/src/peer-info/index.ts | 14 +++++++++++++- packages/kad-dht/src/content-routing/index.ts | 6 ++---- packages/kad-dht/src/kad-dht.ts | 2 +- packages/kad-dht/src/message/index.ts | 3 +-- packages/kad-dht/src/peer-routing/index.ts | 9 +++------ packages/kad-dht/src/rpc/handlers/find-node.ts | 3 +-- packages/kad-dht/src/rpc/handlers/get-providers.ts | 3 +-- packages/kad-dht/test/kad-utils.spec.ts | 4 ++-- packages/kad-dht/test/query-self.spec.ts | 6 ++---- packages/kad-dht/test/query.spec.ts | 3 +-- .../kad-dht/test/rpc/handlers/add-provider.spec.ts | 6 ++---- .../kad-dht/test/rpc/handlers/find-node.spec.ts | 9 +++------ .../test/rpc/handlers/get-providers.spec.ts | 6 ++---- .../kad-dht/test/rpc/handlers/get-value.spec.ts | 3 +-- packages/kad-dht/test/utils/test-dht.ts | 3 +-- packages/libp2p/src/libp2p.ts | 6 ++---- packages/libp2p/test/circuit-relay/utils.ts | 3 +-- .../test/content-routing/content-routing.node.ts | 3 +-- packages/peer-discovery-bootstrap/src/index.ts | 3 +-- packages/peer-discovery-mdns/src/query.ts | 3 +-- 21 files changed, 43 insertions(+), 58 deletions(-) diff --git a/packages/interface-compliance-tests/src/mocks/peer-discovery.ts b/packages/interface-compliance-tests/src/mocks/peer-discovery.ts index 69bd2afc29..7fa6abedc1 100644 --- a/packages/interface-compliance-tests/src/mocks/peer-discovery.ts +++ b/packages/interface-compliance-tests/src/mocks/peer-discovery.ts @@ -49,8 +49,7 @@ export class MockDiscovery extends TypedEventEmitter implem this.safeDispatchEvent('peer', { detail: { id: peerId, - multiaddrs: [multiaddr('/ip4/127.0.0.1/tcp/8000')], - protocols: [] + multiaddrs: [multiaddr('/ip4/127.0.0.1/tcp/8000')] } }) }, this.options.discoveryDelay ?? 1000) diff --git a/packages/interface/src/peer-info/index.ts b/packages/interface/src/peer-info/index.ts index 25dbd9f208..5b7c8cce9d 100644 --- a/packages/interface/src/peer-info/index.ts +++ b/packages/interface/src/peer-info/index.ts @@ -1,8 +1,20 @@ import type { PeerId } from '../peer-id/index.js' import type { Multiaddr } from '@multiformats/multiaddr' +/** + * A `PeerInfo` is a lightweight object that represents a remote peer, it can be + * obtained from peer discovery mechanisms, HTTP RPC endpoints, etc. + * + * @see https://docs.libp2p.io/concepts/fundamentals/peers/#peer-info + */ export interface PeerInfo { + /** + * The identifier of the remote peer + */ id: PeerId + + /** + * The multiaddrs a peer is listening on + */ multiaddrs: Multiaddr[] - protocols: string[] } diff --git a/packages/kad-dht/src/content-routing/index.ts b/packages/kad-dht/src/content-routing/index.ts index 2ca2033cd4..569e7942f7 100644 --- a/packages/kad-dht/src/content-routing/index.ts +++ b/packages/kad-dht/src/content-routing/index.ts @@ -64,8 +64,7 @@ export class ContentRouting { const msg = new Message(MESSAGE_TYPE.ADD_PROVIDER, key.multihash.bytes, 0) msg.providerPeers = [{ id: this.components.peerId, - multiaddrs, - protocols: [] + multiaddrs }] let sent = 0 @@ -140,8 +139,7 @@ export class ContentRouting { providers.push({ id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) }) } catch (err: any) { if (err.code !== 'ERR_NOT_FOUND') { diff --git a/packages/kad-dht/src/kad-dht.ts b/packages/kad-dht/src/kad-dht.ts index fb5be07aa9..6efd51cb09 100644 --- a/packages/kad-dht/src/kad-dht.ts +++ b/packages/kad-dht/src/kad-dht.ts @@ -218,7 +218,7 @@ export class DefaultKadDHT extends TypedEventEmitter implem } async onPeerConnect (peerData: PeerInfo): Promise { - this.log('peer %p connected with protocols', peerData.id, peerData.protocols) + this.log('peer %p connected', peerData.id) if (this.lan) { peerData = removePublicAddresses(peerData) diff --git a/packages/kad-dht/src/message/index.ts b/packages/kad-dht/src/message/index.ts index 585f83c108..ef24ee4c40 100644 --- a/packages/kad-dht/src/message/index.ts +++ b/packages/kad-dht/src/message/index.ts @@ -104,7 +104,6 @@ function fromPbPeer (peer: PBMessage.Peer): PeerInfo { return { id: peerIdFromBytes(peer.id), - multiaddrs: (peer.addrs ?? []).map((a) => multiaddr(a)), - protocols: [] + multiaddrs: (peer.addrs ?? []).map((a) => multiaddr(a)) } } diff --git a/packages/kad-dht/src/peer-routing/index.ts b/packages/kad-dht/src/peer-routing/index.ts index b356f86263..e2b2b304b5 100644 --- a/packages/kad-dht/src/peer-routing/index.ts +++ b/packages/kad-dht/src/peer-routing/index.ts @@ -85,8 +85,7 @@ export class PeerRouting { return { id: peerData.id, - multiaddrs: peerData.addresses.map((address) => address.multiaddr), - protocols: [] + multiaddrs: peerData.addresses.map((address) => address.multiaddr) } } @@ -226,8 +225,7 @@ export class PeerRouting { from: this.components.peerId, peer: { id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) } }, options) } catch (err: any) { @@ -296,8 +294,7 @@ export class PeerRouting { output.push({ id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) }) } catch (err: any) { if (err.code !== 'ERR_NOT_FOUND') { diff --git a/packages/kad-dht/src/rpc/handlers/find-node.ts b/packages/kad-dht/src/rpc/handlers/find-node.ts index 979ed1919a..4763ba5376 100644 --- a/packages/kad-dht/src/rpc/handlers/find-node.ts +++ b/packages/kad-dht/src/rpc/handlers/find-node.ts @@ -48,8 +48,7 @@ export class FindNodeHandler implements DHTMessageHandler { if (uint8ArrayEquals(this.components.peerId.toBytes(), msg.key)) { closer = [{ id: this.components.peerId, - multiaddrs: this.components.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code)), - protocols: [] + multiaddrs: this.components.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code)) }] } else { closer = await this.peerRouting.getCloserPeersOffline(msg.key, peerId) diff --git a/packages/kad-dht/src/rpc/handlers/get-providers.ts b/packages/kad-dht/src/rpc/handlers/get-providers.ts index 8e0c8563bb..18778887a6 100644 --- a/packages/kad-dht/src/rpc/handlers/get-providers.ts +++ b/packages/kad-dht/src/rpc/handlers/get-providers.ts @@ -86,8 +86,7 @@ export class GetProvidersHandler implements DHTMessageHandler { const peerAfterFilter = addrFilter({ id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) }) if (peerAfterFilter.multiaddrs.length > 0) { diff --git a/packages/kad-dht/test/kad-utils.spec.ts b/packages/kad-dht/test/kad-utils.spec.ts index 2d99cc4ebf..4f27ae4a27 100644 --- a/packages/kad-dht/test/kad-utils.spec.ts +++ b/packages/kad-dht/test/kad-utils.spec.ts @@ -73,7 +73,7 @@ describe('kad utils', () => { multiaddr('/dns4/localhost/tcp/4001') ] - const peerInfo = utils.removePrivateAddresses({ id, multiaddrs, protocols: [] }) + const peerInfo = utils.removePrivateAddresses({ id, multiaddrs }) expect(peerInfo.multiaddrs.map((ma) => ma.toString())) .to.eql(['/dns4/example.com/tcp/4001', '/ip4/1.1.1.1/tcp/4001']) }) @@ -90,7 +90,7 @@ describe('kad utils', () => { multiaddr('/dns4/localhost/tcp/4001') ] - const peerInfo = utils.removePublicAddresses({ id, multiaddrs, protocols: [] }) + const peerInfo = utils.removePublicAddresses({ id, multiaddrs }) expect(peerInfo.multiaddrs.map((ma) => ma.toString())) .to.eql(['/ip4/192.168.0.1/tcp/4001', '/dns4/localhost/tcp/4001']) }) diff --git a/packages/kad-dht/test/query-self.spec.ts b/packages/kad-dht/test/query-self.spec.ts index d43313d29e..2c4904276b 100644 --- a/packages/kad-dht/test/query-self.spec.ts +++ b/packages/kad-dht/test/query-self.spec.ts @@ -78,8 +78,7 @@ describe('Query Self', () => { from: remotePeer, peer: { id: remotePeer, - multiaddrs: [], - protocols: [] + multiaddrs: [] } }) }()) @@ -110,8 +109,7 @@ describe('Query Self', () => { from: remotePeer, peer: { id: remotePeer, - multiaddrs: [], - protocols: [] + multiaddrs: [] } }) }()) diff --git a/packages/kad-dht/test/query.spec.ts b/packages/kad-dht/test/query.spec.ts index e1fd7460af..f7e661803c 100644 --- a/packages/kad-dht/test/query.spec.ts +++ b/packages/kad-dht/test/query.spec.ts @@ -534,8 +534,7 @@ describe('QueryManager', () => { messageType: MESSAGE_TYPE.GET_VALUE, closer: [{ id: peers[2], - multiaddrs: [], - protocols: [] + multiaddrs: [] }] }) } diff --git a/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts index ad9633856d..652060570c 100644 --- a/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts @@ -68,12 +68,10 @@ describe('rpc - handlers - AddProvider', () => { msg.providerPeers = [{ id: peerIds[0], - multiaddrs: [ma1], - protocols: [] + multiaddrs: [ma1] }, { id: peerIds[1], - multiaddrs: [ma2], - protocols: [] + multiaddrs: [ma2] }] await handler.handle(peerIds[0], msg) diff --git a/packages/kad-dht/test/rpc/handlers/find-node.spec.ts b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts index 5eb13982bf..e197e3770a 100644 --- a/packages/kad-dht/test/rpc/handlers/find-node.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts @@ -71,8 +71,7 @@ describe('rpc - handlers - FindNode', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/221.4.67.0/tcp/4002') - ], - protocols: [] + ] }]) const response = await handler.handle(sourcePeer, msg) @@ -109,8 +108,7 @@ describe('rpc - handlers - FindNode', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/221.4.67.0/tcp/4002') - ], - protocols: [] + ] }]) handler = new FindNodeHandler({ @@ -146,8 +144,7 @@ describe('rpc - handlers - FindNode', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/221.4.67.0/tcp/4002') - ], - protocols: [] + ] }]) const response = await handler.handle(sourcePeer, msg) diff --git a/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts index 18a3706f64..97688407d4 100644 --- a/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts @@ -73,8 +73,7 @@ describe('rpc - handlers - GetProviders', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.2.6/tcp/4002'), multiaddr('/ip4/21.31.57.23/tcp/4002') - ], - protocols: [] + ] }] const provider: PeerInfo[] = [{ @@ -83,8 +82,7 @@ describe('rpc - handlers - GetProviders', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/135.4.67.0/tcp/4002') - ], - protocols: [] + ] }] providers.getProviders.withArgs(v.cid).resolves([providerPeer]) diff --git a/packages/kad-dht/test/rpc/handlers/get-value.spec.ts b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts index ad79dda7c6..61bf6a35aa 100644 --- a/packages/kad-dht/test/rpc/handlers/get-value.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts @@ -94,8 +94,7 @@ describe('rpc - handlers - GetValue', () => { peerRouting.getCloserPeersOffline.withArgs(key, sourcePeer) .resolves([{ id: closerPeer, - multiaddrs: [], - protocols: [] + multiaddrs: [] }]) const msg = new Message(T, key, 0) diff --git a/packages/kad-dht/test/utils/test-dht.ts b/packages/kad-dht/test/utils/test-dht.ts index bfddebdaad..6d81567526 100644 --- a/packages/kad-dht/test/utils/test-dht.ts +++ b/packages/kad-dht/test/utils/test-dht.ts @@ -94,8 +94,7 @@ export class TestDHT { } components.peerStore.merge(peerData.id, { - multiaddrs: peerData.multiaddrs, - protocols: peerData.protocols + multiaddrs: peerData.multiaddrs }) .catch(err => { log.error(err) }) }) diff --git a/packages/libp2p/src/libp2p.ts b/packages/libp2p/src/libp2p.ts index 05cb801e2b..2aaf92caf5 100644 --- a/packages/libp2p/src/libp2p.ts +++ b/packages/libp2p/src/libp2p.ts @@ -93,8 +93,7 @@ export class Libp2pNode> extends if (evt.detail.previous == null) { const peerInfo: PeerInfo = { id: evt.detail.peer.id, - multiaddrs: evt.detail.peer.addresses.map(a => a.multiaddr), - protocols: evt.detail.peer.protocols + multiaddrs: evt.detail.peer.addresses.map(a => a.multiaddr) } components.events.safeDispatchEvent('peer:discovery', { detail: peerInfo }) @@ -377,8 +376,7 @@ export class Libp2pNode> extends } void this.components.peerStore.merge(peer.id, { - multiaddrs: peer.multiaddrs, - protocols: peer.protocols + multiaddrs: peer.multiaddrs }) .catch(err => { log.error(err) }) } diff --git a/packages/libp2p/test/circuit-relay/utils.ts b/packages/libp2p/test/circuit-relay/utils.ts index aa3cde6e15..b530b8d241 100644 --- a/packages/libp2p/test/circuit-relay/utils.ts +++ b/packages/libp2p/test/circuit-relay/utils.ts @@ -159,8 +159,7 @@ export class MockContentRouting implements ContentRouting { providers.push({ id: this.peerId, - multiaddrs: this.addressManager.getAddresses(), - protocols: [] + multiaddrs: this.addressManager.getAddresses() }) MockContentRouting.providers.set(cid.toString(), providers) diff --git a/packages/libp2p/test/content-routing/content-routing.node.ts b/packages/libp2p/test/content-routing/content-routing.node.ts index 66967667ea..64f95d5616 100644 --- a/packages/libp2p/test/content-routing/content-routing.node.ts +++ b/packages/libp2p/test/content-routing/content-routing.node.ts @@ -251,8 +251,7 @@ describe('content-routing', () => { id: providerPeerId, multiaddrs: [ multiaddr('/ip4/123.123.123.123/tcp/49320') - ], - protocols: [] + ] } if (node.services.dht == null) { diff --git a/packages/peer-discovery-bootstrap/src/index.ts b/packages/peer-discovery-bootstrap/src/index.ts index 999b14f5e0..f7e3d22da5 100644 --- a/packages/peer-discovery-bootstrap/src/index.ts +++ b/packages/peer-discovery-bootstrap/src/index.ts @@ -134,8 +134,7 @@ class Bootstrap extends TypedEventEmitter implements PeerDi const peerData: PeerInfo = { id: peerIdFromString(peerIdStr), - multiaddrs: [ma], - protocols: [] + multiaddrs: [ma] } this.list.push(peerData) diff --git a/packages/peer-discovery-mdns/src/query.ts b/packages/peer-discovery-mdns/src/query.ts index bb6aa8ba30..45f287a705 100644 --- a/packages/peer-discovery-mdns/src/query.ts +++ b/packages/peer-discovery-mdns/src/query.ts @@ -74,8 +74,7 @@ export function gotResponse (rsp: ResponsePacket, localPeerName: string, service return { id: peerIdFromString(peerId), - multiaddrs: multiaddrs.map(addr => addr.decapsulateCode(protocols('p2p').code)), - protocols: [] + multiaddrs: multiaddrs.map(addr => addr.decapsulateCode(protocols('p2p').code)) } } catch (e) { log.error('failed to parse mdns response', e) From 88b3f2e160444416e169b1c8bb7c08dc5698079b Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Wed, 25 Oct 2023 06:26:42 +0100 Subject: [PATCH 7/9] refactor!: remove isStarted method from Startable (#2145) We have an `isStarted` method on the `Startable` interface but we only really use it in tests. Our implementations tend to guard on being started twice so it just adds noise to every implementation. BREAKING CHANGE: the `isStarted` method has been removed from the `Startable` interface --- .../src/pubsub/api.ts | 2 - packages/interface/src/startable.ts | 2 - packages/libp2p/test/core/start.spec.ts | 47 ----- packages/libp2p/test/identify/service.spec.ts | 2 - .../test/transports/transport-manager.spec.ts | 3 - packages/peer-discovery-mdns/src/index.ts | 183 +----------------- packages/peer-discovery-mdns/src/mdns.ts | 158 +++++++++++++++ .../test/compliance.spec.ts | 14 +- .../test/multicast-dns.spec.ts | 3 +- 9 files changed, 169 insertions(+), 245 deletions(-) delete mode 100644 packages/libp2p/test/core/start.spec.ts create mode 100644 packages/peer-discovery-mdns/src/mdns.ts diff --git a/packages/interface-compliance-tests/src/pubsub/api.ts b/packages/interface-compliance-tests/src/pubsub/api.ts index a879c46473..4c81f4f595 100644 --- a/packages/interface-compliance-tests/src/pubsub/api.ts +++ b/packages/interface-compliance-tests/src/pubsub/api.ts @@ -48,7 +48,6 @@ export default (common: TestSetup): void => { await start(...Object.values(components)) - expect(pubsub.isStarted()).to.equal(true) expect(components.registrar.register).to.have.property('callCount', 1) }) @@ -62,7 +61,6 @@ export default (common: TestSetup): void => { await start(...Object.values(components)) await stop(...Object.values(components)) - expect(pubsub.isStarted()).to.equal(false) expect(components.registrar.unregister).to.have.property('callCount', 1) }) diff --git a/packages/interface/src/startable.ts b/packages/interface/src/startable.ts index 8393abc489..7923b1d557 100644 --- a/packages/interface/src/startable.ts +++ b/packages/interface/src/startable.ts @@ -2,8 +2,6 @@ * Implemented by components that have a lifecycle */ export interface Startable { - isStarted(): boolean - /** * If implemented, this method will be invoked before the start method. * diff --git a/packages/libp2p/test/core/start.spec.ts b/packages/libp2p/test/core/start.spec.ts deleted file mode 100644 index a8c5b44855..0000000000 --- a/packages/libp2p/test/core/start.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-env mocha */ - -import { webSockets } from '@libp2p/websockets' -import { expect } from 'aegir/chai' -import { createLibp2p, type Libp2p } from '../../src/index.js' -import { plaintext } from '../../src/insecure/index.js' - -describe('start', () => { - let libp2p: Libp2p - - afterEach(async () => { - if (libp2p != null) { - await libp2p.stop() - } - }) - - it('it should start by default', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - expect(libp2p.isStarted()).to.be.true() - }) - - it('it should allow overriding', async () => { - libp2p = await createLibp2p({ - start: false, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - expect(libp2p.isStarted()).to.be.false() - - await libp2p.start() - - expect(libp2p.isStarted()).to.be.true() - }) -}) diff --git a/packages/libp2p/test/identify/service.spec.ts b/packages/libp2p/test/identify/service.spec.ts index 64e7303e2d..5181b8cdfc 100644 --- a/packages/libp2p/test/identify/service.spec.ts +++ b/packages/libp2p/test/identify/service.spec.ts @@ -153,7 +153,6 @@ describe('identify', () => { // Wait for identify to finish await identityServiceIdentifySpy.firstCall.returnValue - sinon.stub(libp2p, 'isStarted').returns(true) // Cause supported protocols to change await libp2p.handle('/echo/2.0.0', () => {}) @@ -251,7 +250,6 @@ describe('identify', () => { // Wait for identify to finish await identityServiceIdentifySpy.firstCall.returnValue - sinon.stub(libp2p, 'isStarted').returns(true) await libp2p.peerStore.merge(libp2p.peerId, { multiaddrs: [multiaddr('/ip4/180.0.0.1/tcp/15001/ws')] diff --git a/packages/libp2p/test/transports/transport-manager.spec.ts b/packages/libp2p/test/transports/transport-manager.spec.ts index c71a124a92..075364aea9 100644 --- a/packages/libp2p/test/transports/transport-manager.spec.ts +++ b/packages/libp2p/test/transports/transport-manager.spec.ts @@ -124,7 +124,6 @@ describe('libp2p.transportManager (dial only)', () => { start: false }) - expect(libp2p.isStarted()).to.be.false() await expect(libp2p.start()).to.eventually.be.rejected .with.property('code', ErrorCodes.ERR_NO_VALID_ADDRESSES) }) @@ -147,7 +146,6 @@ describe('libp2p.transportManager (dial only)', () => { start: false }) - expect(libp2p.isStarted()).to.be.false() await expect(libp2p.start()).to.eventually.be.undefined() }) @@ -169,7 +167,6 @@ describe('libp2p.transportManager (dial only)', () => { start: false }) - expect(libp2p.isStarted()).to.be.false() await expect(libp2p.start()).to.eventually.be.undefined() }) }) diff --git a/packages/peer-discovery-mdns/src/index.ts b/packages/peer-discovery-mdns/src/index.ts index ed37439b57..6f78e6f7e2 100644 --- a/packages/peer-discovery-mdns/src/index.ts +++ b/packages/peer-discovery-mdns/src/index.ts @@ -76,186 +76,9 @@ * ``` */ -import { CustomEvent, TypedEventEmitter } from '@libp2p/interface/events' -import { peerDiscovery } from '@libp2p/interface/peer-discovery' -import { logger } from '@libp2p/logger' -import multicastDNS from 'multicast-dns' -import * as query from './query.js' -import { stringGen } from './utils.js' -import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface/peer-discovery' -import type { PeerInfo } from '@libp2p/interface/peer-info' -import type { Startable } from '@libp2p/interface/src/startable.js' -import type { AddressManager } from '@libp2p/interface-internal/address-manager' - -const log = logger('libp2p:mdns') - -export interface MulticastDNSInit { - /** - * (true/false) announce our presence through mDNS, default `true` - */ - broadcast?: boolean - - /** - * query interval, default 10 \* 1000 (10 seconds) - */ - interval?: number - - /** - * name of the service announce , default '_p2p._udp.local\` - */ - serviceTag?: string - /** - * Peer name to announce (should not be peeer id), default random string - */ - peerName?: string - - /** - * UDP port to broadcast to - */ - port?: number - - /** - * UDP IP to broadcast to - */ - ip?: string -} - -export interface MulticastDNSComponents { - addressManager: AddressManager -} - -class MulticastDNS extends TypedEventEmitter implements PeerDiscovery, Startable { - public mdns?: multicastDNS.MulticastDNS - - private readonly broadcast: boolean - private readonly interval: number - private readonly serviceTag: string - private readonly peerName: string - private readonly port: number - private readonly ip: string - private _queryInterval: ReturnType | null - private readonly components: MulticastDNSComponents - - constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { - super() - - this.broadcast = init.broadcast !== false - this.interval = init.interval ?? (1e3 * 10) - this.serviceTag = init.serviceTag ?? '_p2p._udp.local' - this.ip = init.ip ?? '224.0.0.251' - this.peerName = init.peerName ?? stringGen(63) - // 63 is dns label limit - if (this.peerName.length >= 64) { - throw new Error('Peer name should be less than 64 chars long') - } - this.port = init.port ?? 5353 - this.components = components - this._queryInterval = null - this._onMdnsQuery = this._onMdnsQuery.bind(this) - this._onMdnsResponse = this._onMdnsResponse.bind(this) - this._onMdnsWarning = this._onMdnsWarning.bind(this) - this._onMdnsError = this._onMdnsError.bind(this) - } - - readonly [peerDiscovery] = this - - readonly [Symbol.toStringTag] = '@libp2p/mdns' - - isStarted (): boolean { - return Boolean(this.mdns) - } - - /** - * Start sending queries to the LAN. - * - * @returns {void} - */ - async start (): Promise { - if (this.mdns != null) { - return - } - - this.mdns = multicastDNS({ port: this.port, ip: this.ip }) - this.mdns.on('query', this._onMdnsQuery) - this.mdns.on('response', this._onMdnsResponse) - this.mdns.on('warning', this._onMdnsWarning) - this.mdns.on('error', this._onMdnsError) - - this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) - } - - _onMdnsQuery (event: multicastDNS.QueryPacket): void { - if (this.mdns == null) { - return - } - - log.trace('received incoming mDNS query') - query.gotQuery( - event, - this.mdns, - this.peerName, - this.components.addressManager.getAddresses(), - this.serviceTag, - this.broadcast) - } - - _onMdnsResponse (event: multicastDNS.ResponsePacket): void { - log.trace('received mDNS query response') - - try { - const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) - - if (foundPeer != null) { - log('discovered peer in mDNS query response %p', foundPeer.id) - - this.dispatchEvent(new CustomEvent('peer', { - detail: foundPeer - })) - } - } catch (err) { - log.error('Error processing peer response', err) - } - } - - _onMdnsWarning (err: Error): void { - log.error('mdns warning', err) - } - - _onMdnsError (err: Error): void { - log.error('mdns error', err) - } - - /** - * Stop sending queries to the LAN. - * - * @returns {Promise} - */ - async stop (): Promise { - if (this.mdns == null) { - return - } - - this.mdns.removeListener('query', this._onMdnsQuery) - this.mdns.removeListener('response', this._onMdnsResponse) - this.mdns.removeListener('warning', this._onMdnsWarning) - this.mdns.removeListener('error', this._onMdnsError) - - if (this._queryInterval != null) { - clearInterval(this._queryInterval) - this._queryInterval = null - } - - await new Promise((resolve) => { - if (this.mdns != null) { - this.mdns.destroy(resolve) - } else { - resolve() - } - }) - - this.mdns = undefined - } -} +import { MulticastDNS } from './mdns.js' +import type { MulticastDNSInit, MulticastDNSComponents } from './mdns.js' +import type { PeerDiscovery } from '@libp2p/interface/peer-discovery' export function mdns (init: MulticastDNSInit = {}): (components: MulticastDNSComponents) => PeerDiscovery { return (components: MulticastDNSComponents) => new MulticastDNS(components, init) diff --git a/packages/peer-discovery-mdns/src/mdns.ts b/packages/peer-discovery-mdns/src/mdns.ts new file mode 100644 index 0000000000..cdcd802a46 --- /dev/null +++ b/packages/peer-discovery-mdns/src/mdns.ts @@ -0,0 +1,158 @@ +import { CustomEvent, EventEmitter } from '@libp2p/interface/events' +import { peerDiscovery } from '@libp2p/interface/peer-discovery' +import { logger } from '@libp2p/logger' +import multicastDNS from 'multicast-dns' +import * as query from './query.js' +import { stringGen } from './utils.js' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface/peer-discovery' +import type { PeerInfo } from '@libp2p/interface/peer-info' +import type { Startable } from '@libp2p/interface/src/startable.js' +import type { AddressManager } from '@libp2p/interface-internal/address-manager' + +const log = logger('libp2p:mdns') + +export interface MulticastDNSInit { + broadcast?: boolean + interval?: number + serviceTag?: string + peerName?: string + port?: number + ip?: string +} + +export interface MulticastDNSComponents { + addressManager: AddressManager +} + +export class MulticastDNS extends EventEmitter implements PeerDiscovery, Startable { + public mdns?: multicastDNS.MulticastDNS + + private readonly broadcast: boolean + private readonly interval: number + private readonly serviceTag: string + private readonly peerName: string + private readonly port: number + private readonly ip: string + private _queryInterval: ReturnType | null + private readonly components: MulticastDNSComponents + + constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { + super() + + this.broadcast = init.broadcast !== false + this.interval = init.interval ?? (1e3 * 10) + this.serviceTag = init.serviceTag ?? '_p2p._udp.local' + this.ip = init.ip ?? '224.0.0.251' + this.peerName = init.peerName ?? stringGen(63) + // 63 is dns label limit + if (this.peerName.length >= 64) { + throw new Error('Peer name should be less than 64 chars long') + } + this.port = init.port ?? 5353 + this.components = components + this._queryInterval = null + this._onMdnsQuery = this._onMdnsQuery.bind(this) + this._onMdnsResponse = this._onMdnsResponse.bind(this) + this._onMdnsWarning = this._onMdnsWarning.bind(this) + this._onMdnsError = this._onMdnsError.bind(this) + } + + readonly [peerDiscovery] = this + + readonly [Symbol.toStringTag] = '@libp2p/mdns' + + isStarted (): boolean { + return Boolean(this.mdns) + } + + /** + * Start sending queries to the LAN. + * + * @returns {void} + */ + async start (): Promise { + if (this.mdns != null) { + return + } + + this.mdns = multicastDNS({ port: this.port, ip: this.ip }) + this.mdns.on('query', this._onMdnsQuery) + this.mdns.on('response', this._onMdnsResponse) + this.mdns.on('warning', this._onMdnsWarning) + this.mdns.on('error', this._onMdnsError) + + this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) + } + + _onMdnsQuery (event: multicastDNS.QueryPacket): void { + if (this.mdns == null) { + return + } + + log.trace('received incoming mDNS query') + query.gotQuery( + event, + this.mdns, + this.peerName, + this.components.addressManager.getAddresses(), + this.serviceTag, + this.broadcast) + } + + _onMdnsResponse (event: multicastDNS.ResponsePacket): void { + log.trace('received mDNS query response') + + try { + const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) + + if (foundPeer != null) { + log('discovered peer in mDNS query response %p', foundPeer.id) + + this.dispatchEvent(new CustomEvent('peer', { + detail: foundPeer + })) + } + } catch (err) { + log.error('Error processing peer response', err) + } + } + + _onMdnsWarning (err: Error): void { + log.error('mdns warning', err) + } + + _onMdnsError (err: Error): void { + log.error('mdns error', err) + } + + /** + * Stop sending queries to the LAN. + * + * @returns {Promise} + */ + async stop (): Promise { + if (this.mdns == null) { + return + } + + this.mdns.removeListener('query', this._onMdnsQuery) + this.mdns.removeListener('response', this._onMdnsResponse) + this.mdns.removeListener('warning', this._onMdnsWarning) + this.mdns.removeListener('error', this._onMdnsError) + + if (this._queryInterval != null) { + clearInterval(this._queryInterval) + this._queryInterval = null + } + + await new Promise((resolve) => { + if (this.mdns != null) { + this.mdns.destroy(resolve) + } else { + resolve() + } + }) + + this.mdns = undefined + } +} diff --git a/packages/peer-discovery-mdns/test/compliance.spec.ts b/packages/peer-discovery-mdns/test/compliance.spec.ts index b24d590d91..d5975b654c 100644 --- a/packages/peer-discovery-mdns/test/compliance.spec.ts +++ b/packages/peer-discovery-mdns/test/compliance.spec.ts @@ -1,16 +1,14 @@ /* eslint-env mocha */ import { CustomEvent } from '@libp2p/interface/events' -import { isStartable } from '@libp2p/interface/startable' import tests from '@libp2p/interface-compliance-tests/peer-discovery' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { multiaddr } from '@multiformats/multiaddr' import { stubInterface } from 'ts-sinon' -import { mdns } from '../src/index.js' -import type { PeerDiscovery } from '@libp2p/interface/peer-discovery' +import { MulticastDNS } from '../src/mdns.js' import type { AddressManager } from '@libp2p/interface-internal/address-manager' -let discovery: PeerDiscovery +let discovery: MulticastDNS describe('compliance tests', () => { let intervalId: ReturnType @@ -25,18 +23,18 @@ describe('compliance tests', () => { multiaddr(`/ip4/127.0.0.1/tcp/13921/p2p/${peerId1.toString()}`) ]) - discovery = mdns({ + discovery = new MulticastDNS({ + addressManager + }, { broadcast: false, port: 50001 - })({ - addressManager }) // Trigger discovery const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star' intervalId = setInterval(() => { - if (isStartable(discovery) && !discovery.isStarted()) { + if (!discovery.isStarted()) { return } diff --git a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts index 9955002dd7..1ac9b75056 100644 --- a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts +++ b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts @@ -6,7 +6,8 @@ import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import pWaitFor from 'p-wait-for' import { stubInterface } from 'ts-sinon' -import { mdns, type MulticastDNSComponents } from './../src/index.js' +import { mdns } from './../src/index.js' +import type { MulticastDNSComponents } from './../src/mdns.js' import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { AddressManager } from '@libp2p/interface-internal/address-manager' From ce6ee749d2357e9fadd0880cfff4b4ac6772909e Mon Sep 17 00:00:00 2001 From: Cayman Date: Tue, 31 Oct 2023 06:01:17 -0400 Subject: [PATCH 8/9] refactor!: move pnet into separate package (#2165) - Move pnet module to a separate package - Related to #1913 --- doc/migrations/v0.46-v1.0.0.md | 17 ++++ packages/libp2p/package.json | 8 +- packages/libp2p/src/pnet/README.md | 68 -------------- .../test/connection-manager/direct.node.ts | 13 ++- packages/libp2p/test/fixtures/swarm.key.ts | 3 - .../libp2p/test/upgrading/upgrader.spec.ts | 21 +++-- packages/libp2p/typedoc.json | 1 - packages/pnet/LICENSE | 4 + packages/pnet/LICENSE-APACHE | 5 ++ packages/pnet/LICENSE-MIT | 19 ++++ packages/pnet/README.md | 89 +++++++++++++++++++ packages/pnet/package.json | 67 ++++++++++++++ .../{libp2p/src/pnet => pnet/src}/crypto.ts | 0 .../{libp2p/src/pnet => pnet/src}/errors.ts | 1 + .../{libp2p/src/pnet => pnet/src}/index.ts | 41 ++++++++- .../src/pnet => pnet/src}/key-generator.ts | 0 .../test/pnet => pnet/test}/index.spec.ts | 4 +- packages/pnet/tsconfig.json | 27 ++++++ packages/pnet/typedoc.json | 5 ++ 19 files changed, 292 insertions(+), 101 deletions(-) delete mode 100644 packages/libp2p/src/pnet/README.md delete mode 100644 packages/libp2p/test/fixtures/swarm.key.ts create mode 100644 packages/pnet/LICENSE create mode 100644 packages/pnet/LICENSE-APACHE create mode 100644 packages/pnet/LICENSE-MIT create mode 100644 packages/pnet/README.md create mode 100644 packages/pnet/package.json rename packages/{libp2p/src/pnet => pnet/src}/crypto.ts (100%) rename packages/{libp2p/src/pnet => pnet/src}/errors.ts (83%) rename packages/{libp2p/src/pnet => pnet/src}/index.ts (72%) rename packages/{libp2p/src/pnet => pnet/src}/key-generator.ts (100%) rename packages/{libp2p/test/pnet => pnet/test}/index.spec.ts (96%) create mode 100644 packages/pnet/tsconfig.json create mode 100644 packages/pnet/typedoc.json diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md index 82132e798f..e6b04bddee 100644 --- a/doc/migrations/v0.46-v1.0.0.md +++ b/doc/migrations/v0.46-v1.0.0.md @@ -7,6 +7,7 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` - [AutoNAT](#autonat) - [KeyChain](#keychain) +- [Pnet](#pnet) - [Metrics](#metrics) ## AutoNAT @@ -54,6 +55,22 @@ const libp2p = await createLibp2p({ const keychain: Keychain = libp2p.services.keychain ``` +## Pnet + +The pnet module is now published in its own package. + +**Before** + +```ts +import { preSharedKey, generateKey } from 'libp2p/pnet' +``` + +**After** + +```ts +import { preSharedKey, generateKey } from '@libp2p/pnet' +``` + ## Metrics The following metrics were renamed: diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index efdd79672f..95a04fc341 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -72,10 +72,6 @@ "types": "./dist/src/ping/index.d.ts", "import": "./dist/src/ping/index.js" }, - "./pnet": { - "types": "./dist/src/pnet/index.d.ts", - "import": "./dist/src/pnet/index.js" - }, "./upnp-nat": { "types": "./dist/src/upnp-nat/index.d.ts", "import": "./dist/src/upnp-nat/index.js" @@ -156,8 +152,7 @@ "rate-limiter-flexible": "^3.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6", - "wherearewe": "^2.0.1", - "xsalsa20": "^1.1.0" + "wherearewe": "^2.0.1" }, "devDependencies": { "@chainsafe/libp2p-gossipsub": "^10.0.0", @@ -174,7 +169,6 @@ "@libp2p/mplex": "^9.0.10", "@libp2p/tcp": "^8.0.11", "@libp2p/websockets": "^7.0.11", - "@types/xsalsa20": "^1.1.0", "aegir": "^41.0.2", "execa": "^8.0.1", "go-libp2p": "^1.1.1", diff --git a/packages/libp2p/src/pnet/README.md b/packages/libp2p/src/pnet/README.md deleted file mode 100644 index 94d03c0362..0000000000 --- a/packages/libp2p/src/pnet/README.md +++ /dev/null @@ -1,68 +0,0 @@ -js-libp2p-pnet -================== - -> Connection protection management for libp2p leveraging PSK encryption via XSalsa20. - -**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-pnet. - -## Table of Contents - -- [Usage](#usage) -- [Private Shared Keys](#private-shared-keys) -- [PSK Generation](#psk-generation) - - [From a module using libp2p](#from-a-module-using-libp2p) - - [Programmatically](#programmatically) - -## Usage - -```js -import { createLibp2p } from 'libp2p' -import { preSharedKey, generateKey } from 'libp2p/pnet' - -// Create a Uint8Array and write the swarm key to it -const swarmKey = new Uint8Array(95) -generateKey(swarmKey) - -const node = await createLibp2p({ - // ...other options - connectionProtector: preSharedKey({ - psk: swarmKey - }) -}) -``` - -## Private Shared Keys - -Private Shared Keys are expected to be in the following format: - -``` -/key/swarm/psk/1.0.0/ -/base16/ -dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d -``` - -## PSK Generation - -A utility method has been created to generate a key for your private network. You can -use one of the methods below to generate your key. - -#### From a module using libp2p - -If you have a module locally that depends on libp2p, you can run the following from -that project, assuming the node_modules are installed. - -```console -node -e "import('libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key -``` - -#### Programmatically - -```js -import fs from 'fs' -import { generateKey } from 'libp2p/pnet' - -const swarmKey = new Uint8Array(95) -generateKey(swarmKey) - -fs.writeFileSync('swarm.key', swarmKey) -``` diff --git a/packages/libp2p/test/connection-manager/direct.node.ts b/packages/libp2p/test/connection-manager/direct.node.ts index 4b225596da..a345a5c8d1 100644 --- a/packages/libp2p/test/connection-manager/direct.node.ts +++ b/packages/libp2p/test/connection-manager/direct.node.ts @@ -32,14 +32,11 @@ import { DefaultConnectionManager } from '../../src/connection-manager/index.js' import { codes as ErrorCodes } from '../../src/errors.js' import { plaintext } from '../../src/insecure/index.js' import { createLibp2pNode, type Libp2pNode } from '../../src/libp2p.js' -import { preSharedKey } from '../../src/pnet/index.js' import { DefaultTransportManager } from '../../src/transport-manager.js' -import swarmKey from '../fixtures/swarm.key.js' import type { PeerId } from '@libp2p/interface/peer-id' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' import type { Multiaddr } from '@multiformats/multiaddr' -const swarmKeyBuffer = uint8ArrayFromString(swarmKey) const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws/p2p/QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN') @@ -496,9 +493,11 @@ describe('libp2p.dialer (direct, TCP)', () => { }) it('should use the protectors when provided for connecting', async () => { - const protector: ConnectionProtector = preSharedKey({ - psk: swarmKeyBuffer - })() + const protector: ConnectionProtector = { + async protect (connection) { + return connection + } + } libp2p = await createLibp2pNode({ peerId, @@ -517,8 +516,6 @@ describe('libp2p.dialer (direct, TCP)', () => { const protectorProtectSpy = Sinon.spy(protector, 'protect') - remoteLibp2p.components.connectionProtector = preSharedKey({ psk: swarmKeyBuffer })() - await libp2p.start() const connection = await libp2p.dial(remoteAddr) diff --git a/packages/libp2p/test/fixtures/swarm.key.ts b/packages/libp2p/test/fixtures/swarm.key.ts deleted file mode 100644 index ee524766d1..0000000000 --- a/packages/libp2p/test/fixtures/swarm.key.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default '/key/swarm/psk/1.0.0/\n' + - '/base16/\n' + - '411f0a244cbbc25ecbb2b070d00a1832516ded521eb3ee3aa13189efe2e2b9a2' diff --git a/packages/libp2p/test/upgrading/upgrader.spec.ts b/packages/libp2p/test/upgrading/upgrader.spec.ts index 6f83c08ecd..0c2267d73b 100644 --- a/packages/libp2p/test/upgrading/upgrader.spec.ts +++ b/packages/libp2p/test/upgrading/upgrader.spec.ts @@ -26,10 +26,8 @@ import { type Components, defaultComponents } from '../../src/components.js' import { codes } from '../../src/errors.js' import { createLibp2p } from '../../src/index.js' import { plaintext } from '../../src/insecure/index.js' -import { preSharedKey } from '../../src/pnet/index.js' import { DEFAULT_MAX_OUTBOUND_STREAMS } from '../../src/registrar.js' import { DefaultUpgrader } from '../../src/upgrader.js' -import swarmKey from '../fixtures/swarm.key.js' import type { Libp2p } from '@libp2p/interface' import type { Connection, ConnectionProtector, Stream } from '@libp2p/interface/connection' import type { ConnectionEncrypter, SecuredConnection } from '@libp2p/interface/connection-encrypter' @@ -206,9 +204,12 @@ describe('Upgrader', () => { it('should use a private connection protector when provided', async () => { const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - const protector = preSharedKey({ - psk: uint8ArrayFromString(swarmKey) - })() + const protector: ConnectionProtector = { + async protect (connection) { + return connection + } + } + const protectorProtectSpy = sinon.spy(protector, 'protect') localComponents.connectionProtector = protector @@ -615,6 +616,12 @@ describe('libp2p.upgrader', () => { it('should create an Upgrader', async () => { const deferred = pDefer() + const protector: ConnectionProtector = { + async protect (connection) { + return connection + } + } + libp2p = await createLibp2p({ peerId: peers[0], transports: [ @@ -627,9 +634,7 @@ describe('libp2p.upgrader', () => { connectionEncryption: [ plaintext() ], - connectionProtector: preSharedKey({ - psk: uint8ArrayFromString(swarmKey) - }), + connectionProtector: () => protector, services: { test: (components: any) => { deferred.resolve(components) diff --git a/packages/libp2p/typedoc.json b/packages/libp2p/typedoc.json index 5629b3c79d..dde288fe84 100644 --- a/packages/libp2p/typedoc.json +++ b/packages/libp2p/typedoc.json @@ -6,7 +6,6 @@ "./src/identify/index.ts", "./src/insecure/index.ts", "./src/ping/index.ts", - "./src/pnet/index.ts", "./src/upnp-nat/index.ts" ] } diff --git a/packages/pnet/LICENSE b/packages/pnet/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/pnet/LICENSE @@ -0,0 +1,4 @@ +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/packages/pnet/LICENSE-APACHE b/packages/pnet/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/pnet/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/packages/pnet/LICENSE-MIT b/packages/pnet/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/pnet/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/packages/pnet/README.md b/packages/pnet/README.md new file mode 100644 index 0000000000..3b12436c22 --- /dev/null +++ b/packages/pnet/README.md @@ -0,0 +1,89 @@ +> Connection protection management for libp2p leveraging PSK encryption via XSalsa20. + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of Connection protection management via a shared secret + +# About + +Connection protection management for libp2p leveraging PSK encryption via XSalsa20. + +## Example + +```typescript +import { createLibp2p } from 'libp2p' +import { preSharedKey, generateKey } from '@libp2p/pnet' + +// Create a Uint8Array and write the swarm key to it +const swarmKey = new Uint8Array(95) +generateKey(swarmKey) + +const node = await createLibp2p({ + // ...other options + connectionProtector: preSharedKey({ + psk: swarmKey + }) +}) +``` + +## Private Shared Keys + +Private Shared Keys are expected to be in the following format: + +``` +/key/swarm/psk/1.0.0/ +/base16/ +dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d +``` + +## PSK Generation + +A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key. + +### From a module using libp2p + +If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node\_modules are installed. + +```console +node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key +``` + +### Programmatically + +```js +import fs from 'fs' +import { generateKey } from '@libp2p/pnet' + +const swarmKey = new Uint8Array(95) +generateKey(swarmKey) + +fs.writeFileSync('swarm.key', swarmKey) +``` + +# Install + +```console +$ npm i @libp2p/pnet +``` + +## Browser ` +``` + +# License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-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/packages/pnet/package.json b/packages/pnet/package.json new file mode 100644 index 0000000000..a568b2e111 --- /dev/null +++ b/packages/pnet/package.json @@ -0,0 +1,67 @@ +{ + "name": "@libp2p/pnet", + "version": "1.0.0", + "description": "Implementation of Connection protection management via a shared secret", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/pnet#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "project": true, + "sourceType": "module" + } + }, + "scripts": { + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "lint": "aegir lint", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.2", + "it-handshake": "^4.1.3", + "it-map": "^3.0.3", + "it-pair": "^2.0.6", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "uint8arrays": "^4.0.6", + "xsalsa20": "^1.1.0" + }, + "devDependencies": { + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/peer-id-factory": "^3.0.5", + "@multiformats/multiaddr": "^12.1.5", + "@types/xsalsa20": "^1.1.0", + "aegir": "^41.0.2", + "it-all": "^3.0.1" + } +} diff --git a/packages/libp2p/src/pnet/crypto.ts b/packages/pnet/src/crypto.ts similarity index 100% rename from packages/libp2p/src/pnet/crypto.ts rename to packages/pnet/src/crypto.ts diff --git a/packages/libp2p/src/pnet/errors.ts b/packages/pnet/src/errors.ts similarity index 83% rename from packages/libp2p/src/pnet/errors.ts rename to packages/pnet/src/errors.ts index b09f68a2a1..a8288ac045 100644 --- a/packages/libp2p/src/pnet/errors.ts +++ b/packages/pnet/src/errors.ts @@ -3,3 +3,4 @@ export const INVALID_PSK = 'Your private shared key is invalid' export const NO_LOCAL_ID = 'No local private key provided' export const NO_HANDSHAKE_CONNECTION = 'No connection for the handshake provided' export const STREAM_ENDED = 'Stream ended prematurely' +export const ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS' diff --git a/packages/libp2p/src/pnet/index.ts b/packages/pnet/src/index.ts similarity index 72% rename from packages/libp2p/src/pnet/index.ts rename to packages/pnet/src/index.ts index 3433710512..c072e47de8 100644 --- a/packages/libp2p/src/pnet/index.ts +++ b/packages/pnet/src/index.ts @@ -7,7 +7,7 @@ * * ```typescript * import { createLibp2p } from 'libp2p' - * import { preSharedKey, generateKey } from 'libp2p/pnet' + * import { preSharedKey, generateKey } from '@libp2p/pnet' * * // Create a Uint8Array and write the swarm key to it * const swarmKey = new Uint8Array(95) @@ -20,6 +20,40 @@ * }) * }) * ``` + * + * ## Private Shared Keys + * + * Private Shared Keys are expected to be in the following format: + * + * ``` + * /key/swarm/psk/1.0.0/ + * /base16/ + * dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d + * ``` + * + * ## PSK Generation + * + * A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key. + * + * ### From a module using libp2p + * + * If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node_modules are installed. + * + * ```console + * node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key + * ``` + * + * ### Programmatically + * + * ```js + * import fs from 'fs' + * import { generateKey } from '@libp2p/pnet' + * + * const swarmKey = new Uint8Array(95) + * generateKey(swarmKey) + * + * fs.writeFileSync('swarm.key', swarmKey) + * ``` */ import { randomBytes } from '@libp2p/crypto' @@ -29,7 +63,6 @@ import { handshake } from 'it-handshake' import map from 'it-map' import { duplexPair } from 'it-pair/duplex' import { pipe } from 'it-pipe' -import { codes } from '../errors.js' import { createBoxStream, createUnboxStream, @@ -81,7 +114,7 @@ class PreSharedKeyConnectionProtector implements ConnectionProtector { } if (connection == null) { - throw new CodeError(Errors.NO_HANDSHAKE_CONNECTION, codes.ERR_INVALID_PARAMETERS) + throw new CodeError(Errors.NO_HANDSHAKE_CONNECTION, Errors.ERR_INVALID_PARAMETERS) } // Exchange nonces @@ -94,7 +127,7 @@ class PreSharedKeyConnectionProtector implements ConnectionProtector { const result = await shake.reader.next(NONCE_LENGTH) if (result.value == null) { - throw new CodeError(Errors.STREAM_ENDED, codes.ERR_INVALID_PARAMETERS) + throw new CodeError(Errors.STREAM_ENDED, Errors.ERR_INVALID_PARAMETERS) } const remoteNonce = result.value.slice() diff --git a/packages/libp2p/src/pnet/key-generator.ts b/packages/pnet/src/key-generator.ts similarity index 100% rename from packages/libp2p/src/pnet/key-generator.ts rename to packages/pnet/src/key-generator.ts diff --git a/packages/libp2p/test/pnet/index.spec.ts b/packages/pnet/test/index.spec.ts similarity index 96% rename from packages/libp2p/test/pnet/index.spec.ts rename to packages/pnet/test/index.spec.ts index 20812893a0..e713e16d51 100644 --- a/packages/libp2p/test/pnet/index.spec.ts +++ b/packages/pnet/test/index.spec.ts @@ -6,8 +6,8 @@ import { expect } from 'aegir/chai' import all from 'it-all' import { pipe } from 'it-pipe' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { INVALID_PSK } from '../../src/pnet/errors.js' -import { preSharedKey, generateKey } from '../../src/pnet/index.js' +import { INVALID_PSK } from '../src/errors.js' +import { preSharedKey, generateKey } from '../src/index.js' const swarmKeyBuffer = new Uint8Array(95) const wrongSwarmKeyBuffer = new Uint8Array(95) diff --git a/packages/pnet/tsconfig.json b/packages/pnet/tsconfig.json new file mode 100644 index 0000000000..1e17899ddc --- /dev/null +++ b/packages/pnet/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface" + }, + { + "path": "../interface-compliance-tests" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/pnet/typedoc.json b/packages/pnet/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/pnet/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +} From 0cb73689a5945c7c6835c2bfeae07d65be4a587e Mon Sep 17 00:00:00 2001 From: chad Date: Thu, 2 Nov 2023 22:45:36 -0500 Subject: [PATCH 9/9] refactor!: move fetch into seperate package (#1913) --- doc/migrations/v0.46-v1.0.0.md | 17 +++++ packages/interface/src/errors.ts | 3 + .../configuration/protocol-prefix.node.ts | 2 +- packages/protocol-autonat/package.json | 5 +- packages/protocol-fetch/LICENSE | 4 ++ packages/protocol-fetch/LICENSE-APACHE | 5 ++ packages/protocol-fetch/LICENSE-MIT | 19 +++++ .../src/fetch => protocol-fetch}/README.md | 2 +- packages/protocol-fetch/package.json | 72 +++++++++++++++++++ .../fetch => protocol-fetch/src}/constants.ts | 0 .../src/fetch => protocol-fetch/src}/index.ts | 13 ++-- .../src}/pb/proto.proto | 0 .../fetch => protocol-fetch/src}/pb/proto.ts | 0 .../test}/fetch.node.ts | 19 +++-- .../test}/index.spec.ts | 4 +- packages/protocol-fetch/tsconfig.json | 10 +++ packages/protocol-fetch/typedoc.json | 5 ++ 17 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 packages/protocol-fetch/LICENSE create mode 100644 packages/protocol-fetch/LICENSE-APACHE create mode 100644 packages/protocol-fetch/LICENSE-MIT rename packages/{libp2p/src/fetch => protocol-fetch}/README.md (98%) create mode 100644 packages/protocol-fetch/package.json rename packages/{libp2p/src/fetch => protocol-fetch/src}/constants.ts (100%) rename packages/{libp2p/src/fetch => protocol-fetch/src}/index.ts (95%) rename packages/{libp2p/src/fetch => protocol-fetch/src}/pb/proto.proto (100%) rename packages/{libp2p/src/fetch => protocol-fetch/src}/pb/proto.ts (100%) rename packages/{libp2p/test/fetch => protocol-fetch/test}/fetch.node.ts (93%) rename packages/{libp2p/test/fetch => protocol-fetch/test}/index.spec.ts (95%) create mode 100644 packages/protocol-fetch/tsconfig.json create mode 100644 packages/protocol-fetch/typedoc.json diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md index e6b04bddee..13ee735733 100644 --- a/doc/migrations/v0.46-v1.0.0.md +++ b/doc/migrations/v0.46-v1.0.0.md @@ -6,6 +6,7 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` ## Table of Contents - [AutoNAT](#autonat) +- [Fetch](#fetch) - [KeyChain](#keychain) - [Pnet](#pnet) - [Metrics](#metrics) @@ -26,6 +27,22 @@ import { autoNATService } from 'libp2p/autonat' import { autoNATService } from '@libp2p/autonat' ``` +## Fetch + +The Fetch service is now publisehd as it's own package. + +**Before** + +```ts +import { autoNATService } from 'libp2p/fetch' +``` + +**After** + +```ts +import { autoNATService } from '@libp2p/fetch' +``` + ## KeyChain The KeyChain object is no longer included on Libp2p and must be instantiated explicitly if desired. diff --git a/packages/interface/src/errors.ts b/packages/interface/src/errors.ts index 4f5f8b4c3a..b7fb770c07 100644 --- a/packages/interface/src/errors.ts +++ b/packages/interface/src/errors.ts @@ -69,3 +69,6 @@ export class InvalidCryptoTransmissionError extends Error { // Error codes export const ERR_TIMEOUT = 'ERR_TIMEOUT' +export const ERR_INVALID_MESSAGE = 'ERR_INVALID_MESSAGE' +export const ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS' +export const ERR_KEY_ALREADY_EXISTS = 'ERR_KEY_ALREADY_EXISTS' diff --git a/packages/libp2p/test/configuration/protocol-prefix.node.ts b/packages/libp2p/test/configuration/protocol-prefix.node.ts index db4ecea61c..fd6fb53d56 100644 --- a/packages/libp2p/test/configuration/protocol-prefix.node.ts +++ b/packages/libp2p/test/configuration/protocol-prefix.node.ts @@ -1,8 +1,8 @@ /* eslint-env mocha */ +import { type FetchService, fetchService } from '@libp2p/fetch' import { expect } from 'aegir/chai' import { pEvent } from 'p-event' -import { type FetchService, fetchService } from '../../src/fetch/index.js' import { identifyService } from '../../src/identify/index.js' import { createLibp2p } from '../../src/index.js' import { type PingService, pingService } from '../../src/ping/index.js' diff --git a/packages/protocol-autonat/package.json b/packages/protocol-autonat/package.json index 0648c94c11..a7bfd0ab08 100644 --- a/packages/protocol-autonat/package.json +++ b/packages/protocol-autonat/package.json @@ -32,7 +32,6 @@ } }, "scripts": { - "start": "node dist/src/main.js", "build": "aegir build", "test": "aegir test", "clean": "aegir clean", @@ -48,8 +47,8 @@ "dependencies": { "@libp2p/interface": "^0.1.2", "@libp2p/interface-internal": "^0.1.5", - "@libp2p/peer-id": "^3.0.2" , - "@libp2p/peer-id-factory": "^3.0.4" , + "@libp2p/peer-id": "^3.0.2", + "@libp2p/peer-id-factory": "^3.0.4", "@libp2p/logger": "^3.0.2", "@multiformats/multiaddr": "^12.1.5", "it-first": "^3.0.1", diff --git a/packages/protocol-fetch/LICENSE b/packages/protocol-fetch/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/protocol-fetch/LICENSE @@ -0,0 +1,4 @@ +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/packages/protocol-fetch/LICENSE-APACHE b/packages/protocol-fetch/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/protocol-fetch/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/packages/protocol-fetch/LICENSE-MIT b/packages/protocol-fetch/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/protocol-fetch/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/packages/libp2p/src/fetch/README.md b/packages/protocol-fetch/README.md similarity index 98% rename from packages/libp2p/src/fetch/README.md rename to packages/protocol-fetch/README.md index 8d231d2e83..fb38d2f9de 100644 --- a/packages/libp2p/src/fetch/README.md +++ b/packages/protocol-fetch/README.md @@ -16,7 +16,7 @@ The fetch protocol is a simple protocol for requesting a value corresponding to ## Usage -```javascript +```ts import { createLibp2p } from 'libp2p' /** diff --git a/packages/protocol-fetch/package.json b/packages/protocol-fetch/package.json new file mode 100644 index 0000000000..049a8064ac --- /dev/null +++ b/packages/protocol-fetch/package.json @@ -0,0 +1,72 @@ +{ + "name": "@libp2p/fetch", + "version": "1.0.0", + "description": "Implementation of the Fetch Protocol", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/protocol-fetch#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "generate": "protons ./src/pb/index.proto", + "lint": "aegir lint", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/interface": "^0.1.4", + "@libp2p/interface-internal": "^0.1.7", + "@libp2p/logger": "^3.0.4", + "it-first": "^3.0.3", + "it-length-prefixed": "^9.0.3", + "it-pipe": "^3.0.1", + "protons-runtime": "^5.2.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.6" + }, + "devDependencies": { + "@chainsafe/libp2p-noise": "^13.0.2", + "@chainsafe/libp2p-yamux": "^5.0.0", + "@libp2p/interface-compliance-tests": "^4.1.2", + "@libp2p/peer-id-factory": "^3.0.6", + "@libp2p/peer-store": "^9.0.7", + "@libp2p/tcp": "^8.0.10", + "aegir": "^40.0.8", + "datastore-core": "^9.2.3", + "delay": "^6.0.0", + "libp2p": "^0.46.16", + "sinon": "^17.0.1", + "sinon-ts": "^2.0.0" + } +} diff --git a/packages/libp2p/src/fetch/constants.ts b/packages/protocol-fetch/src/constants.ts similarity index 100% rename from packages/libp2p/src/fetch/constants.ts rename to packages/protocol-fetch/src/constants.ts diff --git a/packages/libp2p/src/fetch/index.ts b/packages/protocol-fetch/src/index.ts similarity index 95% rename from packages/libp2p/src/fetch/index.ts rename to packages/protocol-fetch/src/index.ts index b9fad11e92..fe88e0874a 100644 --- a/packages/libp2p/src/fetch/index.ts +++ b/packages/protocol-fetch/src/index.ts @@ -1,12 +1,11 @@ -import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' +import { CodeError, ERR_TIMEOUT, ERR_INVALID_MESSAGE, ERR_INVALID_PARAMETERS, ERR_KEY_ALREADY_EXISTS } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' import first from 'it-first' import * as lp from 'it-length-prefixed' import { pipe } from 'it-pipe' import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' import { toString as uint8arrayToString } from 'uint8arrays/to-string' -import { codes } from '../errors.js' import { PROTOCOL_NAME, PROTOCOL_VERSION } from './constants.js' import { FetchRequest, FetchResponse } from './pb/proto.js' import type { AbortOptions } from '@libp2p/interface' @@ -172,7 +171,7 @@ class DefaultFetchService implements Startable, FetchService { const buf = await first(source) if (buf == null) { - throw new CodeError('No data received', codes.ERR_INVALID_MESSAGE) + throw new CodeError('No data received', ERR_INVALID_MESSAGE) } const response = FetchResponse.decode(buf) @@ -189,11 +188,11 @@ class DefaultFetchService implements Startable, FetchService { case (FetchResponse.StatusCode.ERROR): { log('received status for %s error', key) const errmsg = uint8arrayToString(response.data) - throw new CodeError('Error in fetch protocol response: ' + errmsg, codes.ERR_INVALID_PARAMETERS) + throw new CodeError('Error in fetch protocol response: ' + errmsg, ERR_INVALID_PARAMETERS) } default: { log('received status for %s unknown', key) - throw new CodeError('Unknown response status', codes.ERR_INVALID_MESSAGE) + throw new CodeError('Unknown response status', ERR_INVALID_MESSAGE) } } } @@ -224,7 +223,7 @@ class DefaultFetchService implements Startable, FetchService { const buf = await first(source) if (buf == null) { - throw new CodeError('No data received', codes.ERR_INVALID_MESSAGE) + throw new CodeError('No data received', ERR_INVALID_MESSAGE) } // for await (const buf of source) { @@ -280,7 +279,7 @@ class DefaultFetchService implements Startable, FetchService { */ registerLookupFunction (prefix: string, lookup: LookupFunction): void { if (this.lookupFunctions.has(prefix)) { - throw new CodeError(`Fetch protocol handler for key prefix '${prefix}' already registered`, codes.ERR_KEY_ALREADY_EXISTS) + throw new CodeError(`Fetch protocol handler for key prefix '${prefix}' already registered`, ERR_KEY_ALREADY_EXISTS) } this.lookupFunctions.set(prefix, lookup) diff --git a/packages/libp2p/src/fetch/pb/proto.proto b/packages/protocol-fetch/src/pb/proto.proto similarity index 100% rename from packages/libp2p/src/fetch/pb/proto.proto rename to packages/protocol-fetch/src/pb/proto.proto diff --git a/packages/libp2p/src/fetch/pb/proto.ts b/packages/protocol-fetch/src/pb/proto.ts similarity index 100% rename from packages/libp2p/src/fetch/pb/proto.ts rename to packages/protocol-fetch/src/pb/proto.ts diff --git a/packages/libp2p/test/fetch/fetch.node.ts b/packages/protocol-fetch/test/fetch.node.ts similarity index 93% rename from packages/libp2p/test/fetch/fetch.node.ts rename to packages/protocol-fetch/test/fetch.node.ts index 53420c9a45..247e0b1b78 100644 --- a/packages/libp2p/test/fetch/fetch.node.ts +++ b/packages/protocol-fetch/test/fetch.node.ts @@ -1,16 +1,14 @@ /* eslint-env mocha */ import { yamux } from '@chainsafe/libp2p-yamux' -import { mplex } from '@libp2p/mplex' +import { type FetchService, fetchService } from '../src/index.js' import { tcp } from '@libp2p/tcp' import { expect } from 'aegir/chai' -import { codes } from '../../src/errors.js' -import { type FetchService, fetchService } from '../../src/fetch/index.js' -import { createLibp2p } from '../../src/index.js' -import { plaintext } from '../../src/insecure/index.js' -import { createPeerId } from '../fixtures/creators/peer.js' import type { Libp2p } from '@libp2p/interface' import type { PeerId } from '@libp2p/interface/peer-id' +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' async function createNode (peerId: PeerId): Promise> { return createLibp2p({ @@ -24,11 +22,10 @@ async function createNode (peerId: PeerId): Promise { } beforeEach(async () => { - const peerIdA = await createPeerId() - const peerIdB = await createPeerId() + const peerIdA = await createEd25519PeerId() + const peerIdB = await createEd25519PeerId() sender = await createNode(peerIdA) receiver = await createNode(peerIdB) diff --git a/packages/libp2p/test/fetch/index.spec.ts b/packages/protocol-fetch/test/index.spec.ts similarity index 95% rename from packages/libp2p/test/fetch/index.spec.ts rename to packages/protocol-fetch/test/index.spec.ts index dd7d1d921a..fbd03acae8 100644 --- a/packages/libp2p/test/fetch/index.spec.ts +++ b/packages/protocol-fetch/test/index.spec.ts @@ -1,5 +1,6 @@ /* eslint-env mocha */ +import { fetchService, type FetchServiceInit } from '../src/index.js' import { ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' @@ -12,9 +13,6 @@ import delay from 'delay' import { pipe } from 'it-pipe' import sinon from 'sinon' import { stubInterface } from 'sinon-ts' -import { defaultComponents, type Components } from '../../src/components.js' -import { DefaultConnectionManager } from '../../src/connection-manager/index.js' -import { fetchService, type FetchServiceInit } from '../../src/fetch/index.js' import type { ConnectionGater } from '@libp2p/interface/connection-gater' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' diff --git a/packages/protocol-fetch/tsconfig.json b/packages/protocol-fetch/tsconfig.json new file mode 100644 index 0000000000..13a3599639 --- /dev/null +++ b/packages/protocol-fetch/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ] +} diff --git a/packages/protocol-fetch/typedoc.json b/packages/protocol-fetch/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/protocol-fetch/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +}