From 00fd709a7b71e7cf354ea452ebce460dd7375d34 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 13 May 2021 14:55:57 +0100 Subject: [PATCH] feat: allow passing the id of a network peer to ipfs.id (#3386) Adds parity with go-IPFS and allows looking up known peer IDs using the `ipfs.id` command. --- docs/core-api/MISCELLANEOUS.md | 3 +- examples/custom-libp2p/package.json | 2 +- packages/interface-ipfs-core/package.json | 1 + .../src/miscellaneous/id.js | 30 ++++++++ packages/ipfs-cli/src/commands/id.js | 12 +++- packages/ipfs-cli/test/id.js | 21 +++++- packages/ipfs-core-types/src/root.d.ts | 6 +- packages/ipfs-core/package.json | 2 +- packages/ipfs-core/src/components/id.js | 69 +++++++++++-------- packages/ipfs-core/src/components/libp2p.js | 4 ++ packages/ipfs-daemon/package.json | 2 +- packages/ipfs-http-client/src/id.js | 5 +- .../ipfs-http-server/src/api/resources/id.js | 13 +++- packages/ipfs-http-server/test/inject/id.js | 25 ++++++- 14 files changed, 151 insertions(+), 44 deletions(-) diff --git a/docs/core-api/MISCELLANEOUS.md b/docs/core-api/MISCELLANEOUS.md index 6aafaefe2d..61b74fab61 100644 --- a/docs/core-api/MISCELLANEOUS.md +++ b/docs/core-api/MISCELLANEOUS.md @@ -47,6 +47,7 @@ An optional object which may have the following keys: | ---- | ---- | ------- | ----------- | | timeout | `Number` | `undefined` | A timeout in ms | | signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call | +| peerId | `string` | `undefined` | Look up the identity for this peer instead of the current node | ### Returns @@ -300,4 +301,4 @@ A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/package [rs]: https://www.npmjs.com/package/readable-stream [ps]: https://www.npmjs.com/package/pull-stream [cid]: https://www.npmjs.com/package/cids -[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal \ No newline at end of file +[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal diff --git a/examples/custom-libp2p/package.json b/examples/custom-libp2p/package.json index 5c68f936c7..97b35f9bd9 100644 --- a/examples/custom-libp2p/package.json +++ b/examples/custom-libp2p/package.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "ipfs": "^0.55.1", - "libp2p": "^0.31.0", + "libp2p": "^0.31.5", "libp2p-bootstrap": "^0.12.3", "libp2p-kad-dht": "^0.22.0", "libp2p-mdns": "^0.16.0", diff --git a/packages/interface-ipfs-core/package.json b/packages/interface-ipfs-core/package.json index 0fa02f2be6..acbbf3d329 100644 --- a/packages/interface-ipfs-core/package.json +++ b/packages/interface-ipfs-core/package.json @@ -70,6 +70,7 @@ "nanoid": "^3.1.12", "native-abort-controller": "^1.0.3", "p-map": "^4.0.0", + "p-retry": "^4.5.0", "peer-id": "^0.14.1", "readable-stream": "^3.4.0", "uint8arrays": "^2.1.3" diff --git a/packages/interface-ipfs-core/src/miscellaneous/id.js b/packages/interface-ipfs-core/src/miscellaneous/id.js index a0ffc8d769..cd6148fd6a 100644 --- a/packages/interface-ipfs-core/src/miscellaneous/id.js +++ b/packages/interface-ipfs-core/src/miscellaneous/id.js @@ -5,6 +5,7 @@ const { getDescribe, getIt, expect } = require('../utils/mocha') const { Multiaddr } = require('multiaddr') const CID = require('cids') const { isWebWorker } = require('ipfs-utils/src/env') +const retry = require('p-retry') /** @typedef { import("ipfsd-ctl/src/factory") } Factory */ /** @@ -66,5 +67,34 @@ module.exports = (common, options) => { await expect(ipfs.id()).to.eventually.have.property('addresses').that.is.not.empty() }) + + it('should get the id of another node in the swarm', async function () { + if (isWebWorker) { + // TODO: https://github.com/libp2p/js-libp2p-websockets/issues/129 + return this.skip() + } + + const ipfsB = (await common.spawn()).api + await ipfs.swarm.connect(ipfsB.peerId.addresses[0]) + + // have to wait for identify to complete before protocols etc are available for remote hosts + await retry(async () => { + const result = await ipfs.id({ + peerId: ipfsB.peerId.id + }) + + expect(result).to.deep.equal(ipfsB.peerId) + }, { retries: 5 }) + }) + + it('should get our own id when passed as an option', async function () { + const res = await ipfs.id() + + const result = await ipfs.id({ + peerId: res.id + }) + + expect(result).to.deep.equal(res) + }) }) } diff --git a/packages/ipfs-cli/src/commands/id.js b/packages/ipfs-cli/src/commands/id.js index d937204b8d..210be82b7d 100644 --- a/packages/ipfs-cli/src/commands/id.js +++ b/packages/ipfs-cli/src/commands/id.js @@ -3,11 +3,15 @@ const { default: parseDuration } = require('parse-duration') module.exports = { - command: 'id', + command: 'id [peerId]', describe: 'Shows IPFS Node ID info', builder: { + peerid: { + type: 'string', + describe: 'Peer.ID of node to look up' + }, format: { alias: 'f', type: 'string', @@ -24,10 +28,12 @@ module.exports = { * @param {import('../types').Context} argv.ctx * @param {string} argv.format * @param {number} argv.timeout + * @param {string} [argv.peerId] */ - async handler ({ ctx: { ipfs, print }, format, timeout }) { + async handler ({ ctx: { ipfs, print }, format, timeout, peerId }) { const id = await ipfs.id({ - timeout + timeout, + peerId }) if (format) { diff --git a/packages/ipfs-cli/test/id.js b/packages/ipfs-cli/test/id.js index e92a5967dc..47b85b7222 100644 --- a/packages/ipfs-cli/test/id.js +++ b/packages/ipfs-cli/test/id.js @@ -4,9 +4,11 @@ const { expect } = require('aegir/utils/chai') const cli = require('./utils/cli') const sinon = require('sinon') +const PeerId = require('peer-id') const defaultOptions = { - timeout: undefined + timeout: undefined, + peerId: undefined } describe('id', () => { @@ -54,4 +56,21 @@ describe('id', () => { expect(res).to.have.property('id', 'id') expect(res).to.have.property('publicKey', 'publicKey') }) + + it('get the id of another peer', async () => { + const peerId = PeerId.createFromB58String('QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D') + + ipfs.id.withArgs({ + ...defaultOptions, + peerId: peerId.toString() + }).resolves({ + id: 'id', + publicKey: 'publicKey' + }) + + const out = await cli(`id ${peerId}`, { ipfs }) + const res = JSON.parse(out) + expect(res).to.have.property('id', 'id') + expect(res).to.have.property('publicKey', 'publicKey') + }) }) diff --git a/packages/ipfs-core-types/src/root.d.ts b/packages/ipfs-core-types/src/root.d.ts index f33ceb08f8..f6607d431e 100644 --- a/packages/ipfs-core-types/src/root.d.ts +++ b/packages/ipfs-core-types/src/root.d.ts @@ -40,7 +40,7 @@ export interface API { * console.log(identity) * ``` */ - id: (options?: AbortOptions & OptionExtension) => Promise + id: (options?: IDOptions & OptionExtension) => Promise /** * Returns the implementation version @@ -289,6 +289,10 @@ export interface ListOptions extends AbortOptions, PreloadOptions { includeContent?: boolean } +export interface IDOptions extends AbortOptions { + peerId?: string +} + export interface IDResult { id: string publicKey: string diff --git a/packages/ipfs-core/package.json b/packages/ipfs-core/package.json index 5c34784ace..f41e89366f 100644 --- a/packages/ipfs-core/package.json +++ b/packages/ipfs-core/package.json @@ -94,7 +94,7 @@ "it-map": "^1.0.4", "it-pipe": "^1.1.0", "just-safe-set": "^2.2.1", - "libp2p": "^0.31.2", + "libp2p": "^0.31.5", "libp2p-bootstrap": "^0.12.3", "libp2p-crypto": "^0.19.3", "libp2p-floodsub": "^0.25.1", diff --git a/packages/ipfs-core/src/components/id.js b/packages/ipfs-core/src/components/id.js index e4ad10338b..0cfbf49ee0 100644 --- a/packages/ipfs-core/src/components/id.js +++ b/packages/ipfs-core/src/components/id.js @@ -4,6 +4,8 @@ const pkgversion = require('../../package.json').version const { Multiaddr } = require('multiaddr') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const uint8ArrayToString = require('uint8arrays/to-string') +const PeerId = require('peer-id') +const { NotStartedError } = require('../errors') /** * @param {Object} config @@ -14,54 +16,61 @@ module.exports = ({ peerId, network }) => { /** * @type {import('ipfs-core-types/src/root').API["id"]} */ - async function id (_options = {}) { // eslint-disable-line require-await - const id = peerId.toB58String() - /** @type {Multiaddr[]} */ - let addresses = [] - /** @type {string[]} */ - let protocols = [] + async function id (options = {}) { // eslint-disable-line require-await + if (options.peerId === peerId.toB58String()) { + delete options.peerId + } const net = network.try() - if (net) { - const { libp2p } = net - // only available while the node is running - addresses = libp2p.multiaddrs - protocols = Array.from(libp2p.upgrader.protocols.keys()) + if (!net) { + if (options.peerId) { + throw new NotStartedError() + } + + const idStr = peerId.toB58String() + + return { + id: idStr, + publicKey: uint8ArrayToString(peerId.pubKey.bytes, 'base64pad'), + addresses: [], + agentVersion: `js-ipfs/${pkgversion}`, + protocolVersion: '9000', + protocols: [] + } } + const id = options.peerId ? PeerId.createFromB58String(options.peerId.toString()) : peerId + const { libp2p } = net + + const publicKey = options.peerId ? libp2p.peerStore.keyBook.get(id) : id.pubKey + const addresses = options.peerId ? libp2p.peerStore.addressBook.getMultiaddrsForPeer(id) : libp2p.multiaddrs + const protocols = options.peerId ? libp2p.peerStore.protoBook.get(id) : Array.from(libp2p.upgrader.protocols.keys()) + const agentVersion = uint8ArrayToString(libp2p.peerStore.metadataBook.getValue(id, 'AgentVersion') || new Uint8Array()) + const protocolVersion = uint8ArrayToString(libp2p.peerStore.metadataBook.getValue(id, 'ProtocolVersion') || new Uint8Array()) + const idStr = id.toB58String() + return { - id, - publicKey: uint8ArrayToString(peerId.pubKey.bytes, 'base64pad'), - addresses: addresses + id: idStr, + publicKey: uint8ArrayToString(publicKey.bytes, 'base64pad'), + addresses: (addresses || []) .map(ma => { const str = ma.toString() // some relay-style transports add our peer id to the ma for us // so don't double-add - if (str.endsWith(`/p2p/${id}`)) { + if (str.endsWith(`/p2p/${idStr}`)) { return str } - return `${str}/p2p/${id}` + return `${str}/p2p/${idStr}` }) .sort() .map(ma => new Multiaddr(ma)), - agentVersion: `js-ipfs/${pkgversion}`, - protocolVersion: '9000', - protocols: protocols.sort() + agentVersion, + protocolVersion, + protocols: (protocols || []).sort() } } return withTimeoutOption(id) } - -/** - * @typedef {object} ID - * The Peer identity - * @property {string} id - the Peer ID - * @property {string} publicKey - the public key of the peer as a base64 encoded string - * @property {Multiaddr[]} addresses - A list of multiaddrs this node is listening on - * @property {string} agentVersion - The agent version - * @property {string} protocolVersion - The supported protocol version - * @property {string[]} protocols - The supported protocols - */ diff --git a/packages/ipfs-core/src/components/libp2p.js b/packages/ipfs-core/src/components/libp2p.js index e7167009cd..de7098ba09 100644 --- a/packages/ipfs-core/src/components/libp2p.js +++ b/packages/ipfs-core/src/components/libp2p.js @@ -4,6 +4,7 @@ const get = require('dlv') const mergeOptions = require('merge-options') const errCode = require('err-code') const PubsubRouters = require('../runtime/libp2p-pubsub-routers-nodejs') +const pkgversion = require('../../package.json').version /** * @typedef {Object} KeychainConfig @@ -134,6 +135,9 @@ function getLibp2pOptions ({ options, config, datastore, keys, keychainConfig, p keychain: { datastore: keys, ...keychainConfig + }, + host: { + agentVersion: `js-ipfs/${pkgversion}` } } diff --git a/packages/ipfs-daemon/package.json b/packages/ipfs-daemon/package.json index 62f13f8bc3..d646e37e6e 100644 --- a/packages/ipfs-daemon/package.json +++ b/packages/ipfs-daemon/package.json @@ -40,7 +40,7 @@ "ipfs-http-server": "^0.4.0", "ipfs-utils": "^7.0.0", "just-safe-set": "^2.2.1", - "libp2p": "^0.31.2", + "libp2p": "^0.31.5", "libp2p-delegated-content-routing": "^0.10.0", "libp2p-delegated-peer-routing": "^0.9.0", "libp2p-webrtc-star": "^0.22.2", diff --git a/packages/ipfs-http-client/src/id.js b/packages/ipfs-http-client/src/id.js index 6b9c439d5b..ef8aa146d4 100644 --- a/packages/ipfs-http-client/src/id.js +++ b/packages/ipfs-http-client/src/id.js @@ -18,7 +18,10 @@ module.exports = configure(api => { const res = await api.post('id', { timeout: options.timeout, signal: options.signal, - searchParams: toUrlSearchParams(options), + searchParams: toUrlSearchParams({ + arg: options.peerId ? options.peerId.toString() : undefined, + ...options + }), headers: options.headers }) const data = await res.json() diff --git a/packages/ipfs-http-server/src/api/resources/id.js b/packages/ipfs-http-server/src/api/resources/id.js index fa69f797a3..389b0e2064 100644 --- a/packages/ipfs-http-server/src/api/resources/id.js +++ b/packages/ipfs-http-server/src/api/resources/id.js @@ -10,8 +10,13 @@ module.exports = { stripUnknown: true }, query: Joi.object().keys({ - timeout: Joi.timeout() + timeout: Joi.timeout(), + peerId: Joi.string() }) + .rename('arg', 'peerId', { + override: true, + ignoreUndefined: true + }) } }, /** @@ -29,13 +34,15 @@ module.exports = { } }, query: { - timeout + timeout, + peerId } } = request const id = await ipfs.id({ signal, - timeout + timeout, + peerId }) return h.response({ ID: id.id, diff --git a/packages/ipfs-http-server/test/inject/id.js b/packages/ipfs-http-server/test/inject/id.js index 1342611d9d..d8b001496e 100644 --- a/packages/ipfs-http-server/test/inject/id.js +++ b/packages/ipfs-http-server/test/inject/id.js @@ -9,7 +9,8 @@ const { AbortSignal } = require('native-abort-controller') const defaultOptions = { signal: sinon.match.instanceOf(AbortSignal), - timeout: undefined + timeout: undefined, + peerId: undefined } describe('/id', () => { @@ -65,4 +66,26 @@ describe('/id', () => { expect(res).to.have.property('statusCode', 200) }) + + it('get the id of another peer', async () => { + const peerId = 'QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D' + + ipfs.id.withArgs({ + ...defaultOptions, + peerId + }).returns({ + id: 'id', + publicKey: 'publicKey', + addresses: 'addresses', + agentVersion: 'agentVersion', + protocolVersion: 'protocolVersion' + }) + + const res = await http({ + method: 'POST', + url: `/api/v0/id?peerId=${peerId}` + }, { ipfs }) + + expect(res).to.have.property('statusCode', 200) + }) })