From 6a6773d1b4a2bad510c14cacc36ab9215af10fff Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 12 Mar 2024 10:34:32 +0100 Subject: [PATCH 1/5] feat: expose .dns property on @helia/interface Exposes a `.dns` property on the Helia interface for use with other modules such as @helia/ipns. Refs: https://github.com/ipfs/helia-verified-fetch/pull/13#issuecomment-1983944596 --- packages/interface/package.json | 1 + packages/interface/src/index.ts | 7 +++++++ packages/utils/src/index.ts | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/packages/interface/package.json b/packages/interface/package.json index 6a3fee995..b54609812 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -71,6 +71,7 @@ }, "dependencies": { "@libp2p/interface": "^1.1.4", + "@multiformats/dns": "^1.0.1", "interface-blockstore": "^5.2.10", "interface-datastore": "^8.2.11", "interface-store": "^5.1.8", diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index a12024ccb..303dcea1a 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -18,6 +18,7 @@ import type { Blocks } from './blocks.js' import type { Pins } from './pins.js' import type { Routing } from './routing.js' import type { AbortOptions, ComponentLogger } from '@libp2p/interface' +import type { DNS } from '@multiformats/dns' import type { Datastore } from 'interface-datastore' import type { MultihashHasher } from 'multiformats' import type { CID } from 'multiformats/cid' @@ -67,6 +68,12 @@ export interface Helia { */ hashers: Record + /** + * The DNS property can be used to perform lookups of various record types and + * will use a resolver appropriate to the current platform. + */ + dns: DNS + /** * Starts the Helia node */ diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 7890f8795..f0d84cd08 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -19,6 +19,7 @@ import { contentRoutingSymbol, peerRoutingSymbol, start, stop } from '@libp2p/interface' import { defaultLogger } from '@libp2p/logger' +import { dns } from '@multiformats/dns' import drain from 'it-drain' import { CustomProgressEvent } from 'progress-events' import { PinsImpl } from './pins.js' @@ -32,6 +33,7 @@ import type { DAGWalker, GCOptions, Helia as HeliaInterface, Routing } from '@he import type { BlockBroker } from '@helia/interface/blocks' import type { Pins } from '@helia/interface/pins' import type { ComponentLogger, Logger } from '@libp2p/interface' +import type { DNS } from '@multiformats/dns' import type { Blockstore } from 'interface-blockstore' import type { Datastore } from 'interface-datastore' import type { CID } from 'multiformats/cid' @@ -103,6 +105,11 @@ export interface HeliaInit { * Components used by subclasses */ components?: Record + + /** + * An optional DNS implementation used the perform queries. + */ + dns?: DNS } interface Components { @@ -112,6 +119,7 @@ interface Components { dagWalkers: Record logger: ComponentLogger blockBrokers: BlockBroker[] + dns: DNS } export class Helia implements HeliaInterface { @@ -122,6 +130,7 @@ export class Helia implements HeliaInterface { public routing: Routing public dagWalkers: Record public hashers: Record + public dns: DNS private readonly log: Logger constructor (init: HeliaInit) { @@ -129,6 +138,7 @@ export class Helia implements HeliaInterface { this.log = this.logger.forComponent('helia') this.hashers = defaultHashers(init.hashers) this.dagWalkers = defaultDagWalkers(init.dagWalkers) + this.dns = init.dns ?? dns() const components: Components = { blockstore: init.blockstore, @@ -137,6 +147,7 @@ export class Helia implements HeliaInterface { dagWalkers: this.dagWalkers, logger: this.logger, blockBrokers: [], + dns: this.dns, ...(init.components ?? {}) } From f950a79aea3881bc6b2539b5980685a0268f8008 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 12 Mar 2024 10:53:30 +0100 Subject: [PATCH 2/5] chore: typo --- packages/utils/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index f0d84cd08..ef618e7b0 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -107,7 +107,7 @@ export interface HeliaInit { components?: Record /** - * An optional DNS implementation used the perform queries. + * An optional DNS implementation used to perform queries for DNS records. */ dns?: DNS } From 41befc79a3612a681b02b5d08b03f065fec08168 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 12 Mar 2024 13:04:39 +0100 Subject: [PATCH 3/5] chore: upgrade libp2p --- packages/helia/package.json | 2 +- packages/helia/src/index.ts | 1 + .../src/utils/libp2p-defaults.browser.ts | 1 + packages/helia/src/utils/libp2p-defaults.ts | 1 + packages/helia/src/utils/libp2p.ts | 2 + packages/interop/src/ipns-dnslink.spec.ts | 28 ++++++ packages/ipns/src/dnslink.ts | 85 +++++++++++++++++++ packages/utils/package.json | 1 + 8 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 packages/interop/src/ipns-dnslink.spec.ts create mode 100644 packages/ipns/src/dnslink.ts diff --git a/packages/helia/package.json b/packages/helia/package.json index 64d56532e..8c22b4d4c 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -83,7 +83,7 @@ "interface-blockstore": "^5.2.10", "interface-datastore": "^8.2.11", "ipns": "^9.0.0", - "libp2p": "^1.2.4", + "libp2p": "^1.3.0", "multiformats": "^13.1.0" }, "devDependencies": { diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index e03779604..cb4ed8b79 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -100,6 +100,7 @@ export async function createHelia (init: Partial = {}): Promise({ ...init, libp2p: { + dns: init.dns, ...init.libp2p, // ignore the libp2p start parameter as it should be on the main init diff --git a/packages/helia/src/utils/libp2p-defaults.browser.ts b/packages/helia/src/utils/libp2p-defaults.browser.ts index cf7cfe576..0e9b3f2e8 100644 --- a/packages/helia/src/utils/libp2p-defaults.browser.ts +++ b/packages/helia/src/utils/libp2p-defaults.browser.ts @@ -34,6 +34,7 @@ export interface DefaultLibp2pServices extends Record { export function libp2pDefaults (options: Libp2pDefaultsOptions = {}): Libp2pOptions { return { peerId: options.peerId, + dns: options.dns, addresses: { listen: [ '/webrtc' diff --git a/packages/helia/src/utils/libp2p-defaults.ts b/packages/helia/src/utils/libp2p-defaults.ts index 3f4de6002..83bb03b3c 100644 --- a/packages/helia/src/utils/libp2p-defaults.ts +++ b/packages/helia/src/utils/libp2p-defaults.ts @@ -38,6 +38,7 @@ export interface DefaultLibp2pServices extends Record { export function libp2pDefaults (options: Libp2pDefaultsOptions = {}): Libp2pOptions { return { peerId: options.peerId, + dns: options.dns, addresses: { listen: [ '/ip4/0.0.0.0/tcp/0', diff --git a/packages/helia/src/utils/libp2p.ts b/packages/helia/src/utils/libp2p.ts index 64a3b56ee..f5597e08e 100644 --- a/packages/helia/src/utils/libp2p.ts +++ b/packages/helia/src/utils/libp2p.ts @@ -6,6 +6,7 @@ import { libp2pDefaults } from './libp2p-defaults.js' import type { DefaultLibp2pServices } from './libp2p-defaults.js' import type { ComponentLogger, Libp2p, PeerId } from '@libp2p/interface' import type { Keychain, KeychainInit } from '@libp2p/keychain' +import type { DNS } from '@multiformats/dns' import type { Datastore } from 'interface-datastore' import type { Libp2pOptions } from 'libp2p' @@ -20,6 +21,7 @@ export interface CreateLibp2pOptions> { export interface Libp2pDefaultsOptions { peerId?: PeerId keychain?: KeychainInit + dns?: DNS } export async function createLibp2p = DefaultLibp2pServices> (options: CreateLibp2pOptions): Promise> { diff --git a/packages/interop/src/ipns-dnslink.spec.ts b/packages/interop/src/ipns-dnslink.spec.ts new file mode 100644 index 000000000..82db389df --- /dev/null +++ b/packages/interop/src/ipns-dnslink.spec.ts @@ -0,0 +1,28 @@ +/* eslint-env mocha */ + +import { ipns } from '@helia/ipns' +import { createHeliaNode } from './fixtures/create-helia.js' +import type { IPNS } from '@helia/ipns' +import type { HeliaLibp2p } from 'helia' + +describe.only('@helia/ipns - dnslink', () => { + let helia: HeliaLibp2p + let name: IPNS + + beforeEach(async () => { + helia = await createHeliaNode() + name = ipns(helia) + }) + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + }) + + it('should resolve ipfs.io', async () => { + const result = await name.resolveDns('ipfs.io') + + console.info(result) + }) +}) diff --git a/packages/ipns/src/dnslink.ts b/packages/ipns/src/dnslink.ts new file mode 100644 index 000000000..e8d7dcb7c --- /dev/null +++ b/packages/ipns/src/dnslink.ts @@ -0,0 +1,85 @@ +import { CodeError, type Logger } from '@libp2p/interface' +import { peerIdFromString } from '@libp2p/peer-id' +import { RecordType } from '@multiformats/dns' +import { CID } from 'multiformats/cid' +import type { ResolveDNSOptions } from './index.js' +import type { DNS } from '@multiformats/dns' + +const MAX_RECURSIVE_DEPTH = 32 + +export const recursiveResolveDnslink = async (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSOptions = {}): Promise => { + if (depth === 0) { + throw new Error('recursion limit exceeded') + } + + const response = await dns.query(domain, { + ...options, + types: [ + RecordType.TXT + ] + }) + + // TODO: support multiple dnslink records + for (const answer of response.Answer) { + try { + let result = answer.data + + if (!result.startsWith('dnslink=')) { + // invalid record? + continue + } + + result = result.replace('dnslink=', '') + // result is now a `/ipfs/` or `/ipns/` string + const [, protocol, domainOrCID, ...rest] = result.split('/') // e.g. ["", "ipfs", ""] + + if (protocol === 'ipfs') { + try { + const cid = CID.parse(domainOrCID) + + // if the result is a CID, we've reached the end of the recursion + return `/ipfs/${cid}${rest.length > 0 ? `/${rest.join('/')}` : ''}` + } catch {} + } else if (protocol === 'ipns') { + try { + const peerId = peerIdFromString(domainOrCID) + + // if the result is a PeerId, we've reached the end of the recursion + return `/ipns/${peerId}${rest.length > 0 ? `/${rest.join('/')}` : ''}` + } catch {} + + // if the result was another IPNS domain, try to follow it + return await recursiveResolveDnslink(domainOrCID, depth - 1, dns, log, options) + } else { + throw new CodeError(`Unknown protocol in DNSLink record for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND') + } + } catch (err: any) { + log.error('could not parse DNS link record for domain %s, %s', domain, answer.data, err) + } + } + + throw new CodeError(`No DNSLink records found for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND') +} + +export async function resolveDNSLink (domain: string, dns: DNS, log: Logger, options: ResolveDNSOptions = {}): Promise { + try { + return await recursiveResolveDnslink(domain, options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH, dns, log, options) + } catch (err: any) { + // If the code is not ENOTFOUND or ERR_DNSLINK_NOT_FOUND or ENODATA then throw the error + if (err.code !== 'ENOTFOUND' && err.code !== 'ERR_DNSLINK_NOT_FOUND' && err.code !== 'ENODATA') { + throw err + } + + if (domain.startsWith('_dnslink.')) { + // The supplied domain contains a _dnslink component + // Check the non-_dnslink domain + domain = domain.replace('_dnslink.', '') + } else { + // Check the _dnslink subdomain + domain = `_dnslink.${domain}` + } + + // If this throws then we propagate the error + return await recursiveResolveDnslink(domain, options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH, dns, log, options) + } +} diff --git a/packages/utils/package.json b/packages/utils/package.json index 58627f2f7..817d042c1 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -61,6 +61,7 @@ "@libp2p/logger": "^4.0.7", "@libp2p/peer-collections": "^5.1.7", "@libp2p/utils": "^5.2.6", + "@multiformats/dns": "^1.0.1", "any-signal": "^4.1.1", "blockstore-core": "^4.4.0", "cborg": "^4.0.9", From 5ce3a8a1d032ea107392fc23eb65a3990cb80689 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 12 Mar 2024 14:11:47 +0100 Subject: [PATCH 4/5] chore: remove file --- packages/ipns/src/dnslink.ts | 85 ------------------------------------ 1 file changed, 85 deletions(-) delete mode 100644 packages/ipns/src/dnslink.ts diff --git a/packages/ipns/src/dnslink.ts b/packages/ipns/src/dnslink.ts deleted file mode 100644 index e8d7dcb7c..000000000 --- a/packages/ipns/src/dnslink.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CodeError, type Logger } from '@libp2p/interface' -import { peerIdFromString } from '@libp2p/peer-id' -import { RecordType } from '@multiformats/dns' -import { CID } from 'multiformats/cid' -import type { ResolveDNSOptions } from './index.js' -import type { DNS } from '@multiformats/dns' - -const MAX_RECURSIVE_DEPTH = 32 - -export const recursiveResolveDnslink = async (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSOptions = {}): Promise => { - if (depth === 0) { - throw new Error('recursion limit exceeded') - } - - const response = await dns.query(domain, { - ...options, - types: [ - RecordType.TXT - ] - }) - - // TODO: support multiple dnslink records - for (const answer of response.Answer) { - try { - let result = answer.data - - if (!result.startsWith('dnslink=')) { - // invalid record? - continue - } - - result = result.replace('dnslink=', '') - // result is now a `/ipfs/` or `/ipns/` string - const [, protocol, domainOrCID, ...rest] = result.split('/') // e.g. ["", "ipfs", ""] - - if (protocol === 'ipfs') { - try { - const cid = CID.parse(domainOrCID) - - // if the result is a CID, we've reached the end of the recursion - return `/ipfs/${cid}${rest.length > 0 ? `/${rest.join('/')}` : ''}` - } catch {} - } else if (protocol === 'ipns') { - try { - const peerId = peerIdFromString(domainOrCID) - - // if the result is a PeerId, we've reached the end of the recursion - return `/ipns/${peerId}${rest.length > 0 ? `/${rest.join('/')}` : ''}` - } catch {} - - // if the result was another IPNS domain, try to follow it - return await recursiveResolveDnslink(domainOrCID, depth - 1, dns, log, options) - } else { - throw new CodeError(`Unknown protocol in DNSLink record for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND') - } - } catch (err: any) { - log.error('could not parse DNS link record for domain %s, %s', domain, answer.data, err) - } - } - - throw new CodeError(`No DNSLink records found for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND') -} - -export async function resolveDNSLink (domain: string, dns: DNS, log: Logger, options: ResolveDNSOptions = {}): Promise { - try { - return await recursiveResolveDnslink(domain, options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH, dns, log, options) - } catch (err: any) { - // If the code is not ENOTFOUND or ERR_DNSLINK_NOT_FOUND or ENODATA then throw the error - if (err.code !== 'ENOTFOUND' && err.code !== 'ERR_DNSLINK_NOT_FOUND' && err.code !== 'ENODATA') { - throw err - } - - if (domain.startsWith('_dnslink.')) { - // The supplied domain contains a _dnslink component - // Check the non-_dnslink domain - domain = domain.replace('_dnslink.', '') - } else { - // Check the _dnslink subdomain - domain = `_dnslink.${domain}` - } - - // If this throws then we propagate the error - return await recursiveResolveDnslink(domain, options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH, dns, log, options) - } -} From 02e709d1150cf24df3781b78b9de8c59af1a8566 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 12 Mar 2024 14:19:10 +0100 Subject: [PATCH 5/5] chore: fix deps --- packages/helia/package.json | 1 + packages/interop/src/ipns-dnslink.spec.ts | 28 ----------------------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 packages/interop/src/ipns-dnslink.spec.ts diff --git a/packages/helia/package.json b/packages/helia/package.json index 8c22b4d4c..0f535c8b2 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -78,6 +78,7 @@ "@libp2p/webrtc": "^4.0.20", "@libp2p/websockets": "^8.0.16", "@libp2p/webtransport": "^4.0.20", + "@multiformats/dns": "^1.0.1", "blockstore-core": "^4.4.0", "datastore-core": "^9.2.9", "interface-blockstore": "^5.2.10", diff --git a/packages/interop/src/ipns-dnslink.spec.ts b/packages/interop/src/ipns-dnslink.spec.ts deleted file mode 100644 index 82db389df..000000000 --- a/packages/interop/src/ipns-dnslink.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-env mocha */ - -import { ipns } from '@helia/ipns' -import { createHeliaNode } from './fixtures/create-helia.js' -import type { IPNS } from '@helia/ipns' -import type { HeliaLibp2p } from 'helia' - -describe.only('@helia/ipns - dnslink', () => { - let helia: HeliaLibp2p - let name: IPNS - - beforeEach(async () => { - helia = await createHeliaNode() - name = ipns(helia) - }) - - afterEach(async () => { - if (helia != null) { - await helia.stop() - } - }) - - it('should resolve ipfs.io', async () => { - const result = await name.resolveDns('ipfs.io') - - console.info(result) - }) -})