Skip to content

Commit

Permalink
Add DNS package
Browse files Browse the repository at this point in the history
  • Loading branch information
MHHukiewitz committed Mar 25, 2024
1 parent f5ba910 commit 59ffb7b
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 1 deletion.
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"
]
}
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 `⏎`

0 comments on commit 59ffb7b

Please sign in to comment.