Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DNS functionality #165

Merged
merged 3 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"packages/substrate",
"packages/evm",
"packages/superfluid",
"packages/client"
"packages/client",
"packages/dns"
]
}
4 changes: 0 additions & 4 deletions packages/core/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
export const DEFAULT_API_V2 = 'http://pyaleph-lab-2.aleph.cloud:4024/'
export const DEFAULT_API_WS_V2 = 'ws://pyaleph-lab-2.aleph.cloud:4024/'
export const ALEPH_SUPERFLUID_FUJI_TESTNET = '0x1290248E01ED2F9f863A9752A8aAD396ef3a1B00'
export const ALEPH_SUPERFLUID_MAINNET = '0xc0Fbc4967259786C743361a5885ef49380473dCF'
export const SUPERFLUID_FUJI_TESTNET_SUBGRAPH_URL = 'https://avalanche-fuji.subgraph.x.superfluid.dev/'
export const SUPERFLUID_MAINNET_SUBGRAPH_URL = 'https://avalanche-c.subgraph.x.superfluid.dev/'
20 changes: 20 additions & 0 deletions packages/dns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# @aleph-sdk/dns

The `@aleph-sdk/dns` module provides a set of utilities for DNS resolution and manipulation, tailored for use within the Aleph.im ecosystem. It offers functionalities to parse hostnames from URLs, perform DNS queries for various record types, and validate domain configurations against specific criteria.

**This package uses `node:dns`, which needs to be polyfilled, if used in a browser environment.**

## Features

- Parse hostnames from URLs, with or without protocols specified.
- Resolve IPv4 and IPv6 addresses.
- Fetch DNSLink records.
- Validate domain configurations for Aleph.im specific targets (e.g., IPFS files, deployed ASGI programs and instances).

## Installation

You can install `@aleph-sdk/dns` using npm:

```bash
npm install @aleph-sdk/dns
```
51 changes: 51 additions & 0 deletions packages/dns/__tests__/domain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DomainValidator, hostnameFromUrl, TargetType } from '../src/index'

describe('DomainValidator Tests', () => {
test('hostnameFromUrl', () => {
let hostname = hostnameFromUrl('https://aleph.im')
expect(hostname).toBe('aleph.im')
hostname = hostnameFromUrl('aleph.im')
expect(hostname).toBe('aleph.im')
})

test('query A record', async () => {
const alephdns = new DomainValidator()
const hostname = hostnameFromUrl('https://aleph.im')
const query = await alephdns.getIPv4Addresses(hostname) // Adjust method based on actual implementation
expect(query).not.toBeNull()
expect(query.length).toBeGreaterThan(0)
})

test('get IPv6 address', async () => {
const alephdns = new DomainValidator()
const url = 'https://aleph.im'
const hostname = hostnameFromUrl(url)
const ipv6Addresses = await alephdns.getIPv6Addresses(hostname)
expect(ipv6Addresses).not.toBeNull()
expect(ipv6Addresses.length).toBeGreaterThan(0)
expect(ipv6Addresses[0]).toContain(':')
})

test('DNSLink', async () => {
const alephdns = new DomainValidator()
const url = 'https://aleph.im'
const hostname = hostnameFromUrl(url)
const dnslink = await alephdns.getDnsLink(hostname)
expect(dnslink).not.toBeNull()
})

test('configured domain', async () => {
const alephdns = new DomainValidator()
const url = 'https://custom-domain-unit-test.aleph.sh'
const hostname = hostnameFromUrl(url)
const status = await alephdns.checkDomain(hostname, TargetType.IPFS, '0xfakeaddress')
expect(typeof status).toBe('object')
})

test('not configured domain', async () => {
const alephdns = new DomainValidator()
const url = 'https://not-configured-domain.aleph.sh'
const hostname = hostnameFromUrl(url)
await expect(alephdns.checkDomain(hostname, TargetType.IPFS, '0xfakeaddress')).rejects.toThrow()
})
})
21 changes: 21 additions & 0 deletions packages/dns/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@aleph-sdk/dns",
"version": "1.0.0-rc2",
"description": "",
"main": "dist/cjs/index.min.cjs",
"module": "dist/esm/index.min.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"rollup": "rollup -c ../../rollup.config.js",
"build": "npm run rollup"
},
"author": "",
"license": "ISC"
}
5 changes: 5 additions & 0 deletions packages/dns/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const DNS_RESOLVERS = ['9.9.9.9', '1.1.1.1']
export const DNS_IPFS_DOMAIN = 'ipfs.public.aleph.sh'
export const DNS_PROGRAM_DOMAIN = 'program.public.aleph.sh'
export const DNS_INSTANCE_DOMAIN = 'instance.public.aleph.sh'
export const DNS_STATIC_DOMAIN = 'static.public.aleph.sh'
256 changes: 256 additions & 0 deletions packages/dns/src/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { Resolver, resolveNs } from 'dns'
import { URL } from 'url'
import { DomainConfigurationError } from './errors'
import { DNS_RESOLVERS, DNS_STATIC_DOMAIN, DNS_PROGRAM_DOMAIN, DNS_INSTANCE_DOMAIN, DNS_IPFS_DOMAIN } from './constants'
import { TargetType } from './types'

type Hostname = string

class DNSRule {
constructor(
public name: string,
public dns: Record<string, any>,
public info: string,
public on_error: string,
) {}

raiseError(status: Record<string, boolean>): void {
throw new DomainConfigurationError(`${this.info}, ${this.on_error}, ${JSON.stringify(status)}`)
}
}

export function hostnameFromUrl(url: string): Hostname {
if (!url.includes('://')) {
url = `https://${url}`
}
const parsed = new URL(url)
if (parsed.hostname) {
return parsed.hostname
}
throw new Error('Invalid URL')
}

export class DomainValidator {
private resolver: Resolver = new Resolver()

constructor(dnsServers = DNS_RESOLVERS) {
this.resolver.setServers(dnsServers)
}

// This function can be problematic due to TypeScript's type system; might need adjustments
async getIPv4Addresses(hostname: Hostname): Promise<string[]> {
return new Promise((resolve, reject) => {
this.resolver.resolve4(hostname, (err, addresses) => {
if (err) reject(err)
else resolve(addresses)
})
})
}

// Similar to getIPv4Addresses, adjust for IPv6
async getIPv6Addresses(hostname: Hostname): Promise<string[]> {
return new Promise((resolve, reject) => {
this.resolver.resolve6(hostname, (err, addresses) => {
if (err) reject(err)
else resolve(addresses)
})
})
}

async getDnsLinks(hostname: Hostname): Promise<string[]> {
const txtRecords = await new Promise<string[][]>((resolve, reject) => {
this.resolver.resolveTxt(`_dnslink.${hostname}`, (err, records) => {
if (err) reject(err)
else resolve(records)
})
})

return txtRecords.flat().filter((record) => record.startsWith('dnslink='))
}

async getDnsLink(hostname: Hostname): Promise<string | null> {
const dnsLinks = await this.getDnsLinks(hostname)
return dnsLinks.length > 0 ? dnsLinks[0] : null
}

async getTxtValues(hostname: Hostname, delimiter?: string): Promise<string[]> {
const txtRecords = await new Promise<string[][]>((resolve, reject) => {
this.resolver.resolveTxt(hostname, (err, records) => {
if (err) reject(err)
else resolve(records)
})
})

let values: string[] = txtRecords.flat()
if (delimiter) {
values = values.flatMap((record) => record.split(delimiter))
}

return values.filter((value) => value.startsWith('0x')) // Adjust filter condition as needed
}

async getNameServers(hostname: Hostname): Promise<string[]> {
let dnsServers = DNS_RESOLVERS
let fqdn = hostname

// eslint-disable-next-line no-constant-condition
while (true) {
try {
const entries = await new Promise<string[]>((resolve, reject) => {
resolveNs(fqdn, (err, addresses) => {
if (err) reject(err)
else resolve(addresses)
})
})
let servers: string[] = []
for (const entry of entries) {
servers = servers.concat(await this.getIPv6Addresses(entry))
servers = servers.concat(await this.getIPv4Addresses(entry))
}

dnsServers = servers
break
} catch (err) {
const subDomains = fqdn.split('.')
if (subDomains.length > 2) {
fqdn = subDomains.slice(1).join('.')
continue
}

if (subDomains.length === 2) {
break
}

console.debug(`Unexpected error: ${err}, ${typeof err}`)
break
}
}

return dnsServers
}

async getResolverFor(hostname: Hostname): Promise<Resolver> {
const dnsServers = await this.getNameServers(hostname)
const resolver = new Resolver()
resolver.setServers(dnsServers)
return resolver
}

getRequiredDnsRules(hostname: Hostname, target: TargetType, owner?: string): DNSRule[] {
let cnameValue: string | null = null
if (target === TargetType.IPFS) {
cnameValue = DNS_IPFS_DOMAIN
} else if (target === TargetType.PROGRAM) {
cnameValue = `${hostname}.${DNS_PROGRAM_DOMAIN}`
} else if (target === TargetType.INSTANCE) {
cnameValue = `${hostname}.${DNS_INSTANCE_DOMAIN}`
}

const dnsRules: DNSRule[] = []

if (cnameValue) {
dnsRules.push(
new DNSRule(
'cname',
{
type: 'cname',
name: hostname,
value: cnameValue,
},
`Create a CNAME record for ${hostname} with value ${cnameValue}`,
`CNAME record not found: ${hostname}`,
),
)
}

if (target === TargetType.IPFS) {
dnsRules.push(
new DNSRule(
'delegation',
{
type: 'cname',
name: `_dnslink.${hostname}`,
value: `_dnslink.${hostname}.${DNS_STATIC_DOMAIN}`,
},
`Create a CNAME record for _dnslink.${hostname} with value _dnslink.${hostname}.${DNS_STATIC_DOMAIN}`,
`CNAME record not found: _dnslink.${hostname}`,
),
)
}

if (owner) {
dnsRules.push(
new DNSRule(
'owner_proof',
{
type: 'txt',
name: `_control.${hostname}`,
value: owner,
},
`Create a TXT record for _control.${hostname} with value ${owner}`,
'Owner address mismatch',
),
)
}

return dnsRules
}

async checkDomain(hostname: Hostname, target: TargetType, owner?: string): Promise<Record<string, boolean>> {
const status: Record<string, boolean> = {}

const dnsRules = this.getRequiredDnsRules(hostname, target, owner)
const resolver = await this.getResolverFor(hostname) // Ensure this method is correctly implemented to get a DNS resolver
for (const dnsRule of dnsRules) {
status[dnsRule.name] = false

const recordName = dnsRule.dns['name']
const recordType = dnsRule.dns['type']
const recordValue = dnsRule.dns['value']

try {
let entries: string[]
switch (recordType) {
case 'txt':
entries = await new Promise<string[]>((resolve, reject) => {
resolver.resolveTxt(recordName, (err, records) => {
if (err) reject(err)
else resolve(records.flat())
})
})
break
case 'cname':
entries = await new Promise<string[]>((resolve, reject) => {
resolver.resolveCname(recordName, (err, records) => {
if (err) reject(err)
else resolve(records)
})
})
break
// Add cases for other record types as needed
default:
entries = []
break
}

for (const entry of entries) {
const entryValue = Array.isArray(entry) ? entry.join('') : entry // TXT records are arrays
if (entryValue === recordValue) {
status[dnsRule.name] = true
break
}
}
} catch (error) {
console.error(`Failed to query DNS for ${recordName}: ${error}`)
// Continue checks despite errors
}
}

const allTrue = Object.values(status).every((value) => value)
if (!allTrue) {
throw new DomainConfigurationError(`Domain configuration error for ${hostname}`)
}

return status
}
}
6 changes: 6 additions & 0 deletions packages/dns/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class DomainConfigurationError extends Error {
constructor(message: string) {
super(message)
this.name = 'DomainConfigurationError'
}
}
4 changes: 4 additions & 0 deletions packages/dns/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './domain'
export * from './constants'
export * from './errors'
export * from './types'
5 changes: 5 additions & 0 deletions packages/dns/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum TargetType {
IPFS = 'ipfs',
PROGRAM = 'program',
INSTANCE = 'instance',
}

Check failure on line 5 in packages/dns/src/types.ts

View workflow job for this annotation

GitHub Actions / lint

Insert `⏎`
Loading
Loading