/*!
* resource.js - hns records for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const {encoding, wire, util} = require('bns');
const base32 = require('bcrypto/lib/encoding/base32');
const {IP} = require('binet');
const bio = require('bufio');
const key = require('./key');
const nsec = require('./nsec');
const {Struct} = bio;
const {
DUMMY,
DEFAULT_TTL,
TYPE_MAP_EMPTY,
TYPE_MAP_NS,
TYPE_MAP_TXT,
hsTypes
} = require('./common');
const {
sizeName,
writeNameBW,
readNameBR,
sizeString,
writeStringBW,
readStringBR,
isName,
readIP,
writeIP
} = encoding;
const {
Message,
Record,
ARecord,
AAAARecord,
NSRecord,
TXTRecord,
DSRecord,
types
} = wire;
/**
* Resource
* @extends {Struct}
*/
class Resource extends Struct {
constructor() {
super();
this.ttl = DEFAULT_TTL;
this.records = [];
}
hasType(type) {
assert((type & 0xff) === type);
for (const record of this.records) {
if (record.type === type)
return true;
}
return false;
}
hasNS() {
for (const {type} of this.records) {
if (type < hsTypes.NS || type > hsTypes.SYNTH6)
continue;
return true;
}
return false;
}
hasDS() {
return this.hasType(hsTypes.DS);
}
encode() {
const bw = bio.write(512);
this.write(bw, new Map());
return bw.slice();
}
getSize(map) {
let size = 1;
for (const rr of this.records)
size += 1 + rr.getSize(map);
return size;
}
write(bw, map) {
bw.writeU8(0);
for (const rr of this.records) {
bw.writeU8(rr.type);
rr.write(bw, map);
}
return this;
}
read(br) {
const version = br.readU8();
if (version !== 0)
throw new Error(`Unknown serialization version: ${version}.`);
while (br.left()) {
const RD = typeToClass(br.readU8());
// Break at unknown records.
if (!RD)
break;
this.records.push(RD.read(br));
}
return this;
}
toNS(name) {
const authority = [];
const set = new Set();
for (const record of this.records) {
switch (record.type) {
case hsTypes.NS:
case hsTypes.GLUE4:
case hsTypes.GLUE6:
case hsTypes.SYNTH4:
case hsTypes.SYNTH6:
break;
default:
continue;
}
const rr = record.toDNS(name, this.ttl);
if (set.has(rr.data.ns))
continue;
set.add(rr.data.ns);
authority.push(rr);
}
return authority;
}
toGlue(name) {
const additional = [];
for (const record of this.records) {
switch (record.type) {
case hsTypes.GLUE4:
case hsTypes.GLUE6:
if (!util.isSubdomain(name, record.ns))
continue;
break;
case hsTypes.SYNTH4:
case hsTypes.SYNTH6:
break;
default:
continue;
}
additional.push(record.toGlue(record.ns, this.ttl));
}
return additional;
}
toDS(name) {
const answer = [];
for (const record of this.records) {
if (record.type !== hsTypes.DS)
continue;
answer.push(record.toDNS(name, this.ttl));
}
return answer;
}
toTXT(name) {
const answer = [];
for (const record of this.records) {
if (record.type !== hsTypes.TXT)
continue;
answer.push(record.toDNS(name, this.ttl));
}
return answer;
}
toZone(name, sign = false) {
const zone = [];
const set = new Set();
for (const record of this.records) {
const rr = record.toDNS(name, this.ttl);
if (rr.type === types.NS) {
if (set.has(rr.data.ns))
continue;
set.add(rr.data.ns);
}
zone.push(rr);
}
if (sign) {
const set = new Set();
for (const rr of zone)
set.add(rr.type);
const types = [...set].sort();
for (const type of types)
key.signZSK(zone, type);
}
// Add the glue last.
for (const record of this.records) {
switch (record.type) {
case hsTypes.GLUE4:
case hsTypes.GLUE6:
case hsTypes.SYNTH4:
case hsTypes.SYNTH6: {
if (!util.isSubdomain(name, record.ns))
continue;
zone.push(record.toGlue(record.ns, this.ttl));
break;
}
}
}
return zone;
}
toReferral(name, type, isTLD) {
const res = new Message();
// no DS referrals for TLDs
const badReferral = isTLD && type === types.DS;
if (this.hasNS() && !badReferral) {
res.authority = this.toNS(name).concat(this.toDS(name));
res.additional = this.toGlue(name);
if (this.hasDS()) {
key.signZSK(res.authority, types.DS);
} else {
// unsigned zone proof
res.authority.push(this.toNSEC(name));
key.signZSK(res.authority, types.NSEC);
}
} else {
// Needs SOA.
res.aa = true;
// negative answer proof
res.authority.push(this.toNSEC(name));
key.signZSK(res.authority, types.NSEC);
}
return res;
}
toNSEC(name) {
let typeMap = TYPE_MAP_EMPTY;
if (this.hasNS())
typeMap = TYPE_MAP_NS;
else if (this.hasType(hsTypes.TXT))
typeMap = TYPE_MAP_TXT;
return nsec.create(name, nsec.nextName(name), typeMap);
}
toDNS(name, type) {
assert(util.isFQDN(name));
assert((type >>> 0) === type);
const labels = util.split(name);
// Referral.
if (labels.length > 1) {
const tld = util.from(name, labels, -1);
return this.toReferral(tld, type, false);
}
// Potentially an answer.
const res = new Message();
// TLDs are authoritative over their own NS & TXT records.
// The NS records in the root zone are just "hints"
// and therefore are not signed by the root ZSK.
// The only records root is authoritative over is DS.
switch (type) {
case types.TXT:
if (!this.hasNS()) {
res.aa = true;
res.answer = this.toTXT(name);
key.signZSK(res.answer, types.TXT);
}
break;
case types.DS:
res.aa = true;
res.answer = this.toDS(name);
key.signZSK(res.answer, types.DS);
break;
}
// Nope, we may need a referral
if (res.answer.length === 0 && res.authority.length === 0) {
return this.toReferral(name, type, true);
}
return res;
}
getJSON(name) {
const json = { records: [] };
for (const record of this.records)
json.records.push(record.getJSON());
return json;
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid json.');
assert(Array.isArray(json.records), 'Invalid records.');
for (const item of json.records) {
assert(item && typeof item === 'object', 'Invalid record.');
const RD = stringToClass(item.type);
if (!RD)
throw new Error(`Unknown type: ${item.type}.`);
this.records.push(RD.fromJSON(item));
}
return this;
}
}
/**
* DS
* @extends {Struct}
*/
class DS extends Struct {
constructor() {
super();
this.keyTag = 0;
this.algorithm = 0;
this.digestType = 0;
this.digest = DUMMY;
}
get type() {
return hsTypes.DS;
}
getSize() {
return 5 + this.digest.length;
}
write(bw) {
bw.writeU16BE(this.keyTag);
bw.writeU8(this.algorithm);
bw.writeU8(this.digestType);
bw.writeU8(this.digest.length);
bw.writeBytes(this.digest);
return this;
}
read(br) {
this.keyTag = br.readU16BE();
this.algorithm = br.readU8();
this.digestType = br.readU8();
this.digest = br.readBytes(br.readU8());
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
const rr = new Record();
const rd = new DSRecord();
rr.name = name;
rr.type = types.DS;
rr.ttl = ttl;
rr.data = rd;
rd.keyTag = this.keyTag;
rd.algorithm = this.algorithm;
rd.digestType = this.digestType;
rd.digest = this.digest;
return rr;
}
getJSON() {
return {
type: 'DS',
keyTag: this.keyTag,
algorithm: this.algorithm,
digestType: this.digestType,
digest: this.digest.toString('hex')
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid DS record.');
assert(json.type === 'DS',
'Invalid DS record. Type must be "DS".');
assert((json.keyTag & 0xffff) === json.keyTag,
'Invalid DS record. KeyTag must be a uint16.');
assert((json.algorithm & 0xff) === json.algorithm,
'Invalid DS record. Algorithm must be a uint8.');
assert((json.digestType & 0xff) === json.digestType,
'Invalid DS record. DigestType must be a uint8.');
assert(typeof json.digest === 'string',
'Invalid DS record. Digest must be a String.');
assert((json.digest.length >>> 1) <= 255,
'Invalid DS record. Digest is too large.');
this.keyTag = json.keyTag;
this.algorithm = json.algorithm;
this.digestType = json.digestType;
this.digest = util.parseHex(json.digest);
return this;
}
}
/**
* NS
* @extends {Struct}
*/
class NS extends Struct {
constructor() {
super();
this.ns = '.';
}
get type() {
return hsTypes.NS;
}
getSize(map) {
return sizeName(this.ns, map);
}
write(bw, map) {
writeNameBW(bw, this.ns, map);
return this;
}
read(br) {
this.ns = readNameBR(br);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
getJSON() {
return {
type: 'NS',
ns: this.ns
};
}
fromJSON(json) {
assert(json && typeof json === 'object',
'Invalid NS record.');
assert(json.type === 'NS',
'Invalid NS record. Type must be "NS".');
assert(isName(json.ns),
'Invalid NS record. ns must be a valid name.');
this.ns = json.ns;
return this;
}
}
/**
* GLUE4
* @extends {Struct}
*/
class GLUE4 extends Struct {
constructor() {
super();
this.ns = '.';
this.address = '0.0.0.0';
}
get type() {
return hsTypes.GLUE4;
}
getSize(map) {
return sizeName(this.ns, map) + 4;
}
write(bw, map) {
writeNameBW(bw, this.ns, map);
writeIP(bw, this.address, 4);
return this;
}
read(br) {
this.ns = readNameBR(br);
this.address = readIP(br, 4);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createA(name, ttl, this.address);
}
getJSON() {
return {
type: 'GLUE4',
ns: this.ns,
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid GLUE4 record.');
assert(json.type === 'GLUE4',
'Invalid GLUE4 record. Type must be "GLUE4".');
assert(isName(json.ns),
'Invalid GLUE4 record. ns must be a valid name.');
assert(IP.isIPv4String(json.address),
'Invalid GLUE4 record. Address must be a valid IPv4 address.');
this.ns = json.ns;
this.address = IP.normalize(json.address);
return this;
}
}
/**
* GLUE6
* @extends {Struct}
*/
class GLUE6 extends Struct {
constructor() {
super();
this.ns = '.';
this.address = '::';
}
get type() {
return hsTypes.GLUE6;
}
getSize(map) {
return sizeName(this.ns, map) + 16;
}
write(bw, map) {
writeNameBW(bw, this.ns, map);
writeIP(bw, this.address, 16);
return this;
}
read(br) {
this.ns = readNameBR(br);
this.address = readIP(br, 16);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createAAAA(name, ttl, this.address);
}
getJSON() {
return {
type: 'GLUE6',
ns: this.ns,
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid GLUE6 record.');
assert(json.type === 'GLUE6',
'Invalid GLUE6 record. Type must be "GLUE6".');
assert(isName(json.ns),
'Invalid GLUE6 record. ns must be a valid name.');
assert(IP.isIPv6String(json.address),
'Invalid GLUE6 record. Address must be a valid IPv6 address.');
this.ns = json.ns;
this.address = IP.normalize(json.address);
return this;
}
}
/**
* SYNTH4
* @extends {Struct}
*/
class SYNTH4 extends Struct {
constructor() {
super();
this.address = '0.0.0.0';
}
get type() {
return hsTypes.SYNTH4;
}
get ns() {
const ip = IP.toBuffer(this.address).slice(12);
return `_${base32.encodeHex(ip)}._synth.`;
}
getSize() {
return 4;
}
write(bw) {
writeIP(bw, this.address, 4);
return this;
}
read(br) {
this.address = readIP(br, 4);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createA(name, ttl, this.address);
}
getJSON() {
return {
type: 'SYNTH4',
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid SYNTH4 record.');
assert(json.type === 'SYNTH4',
'Invalid SYNTH4 record. Type must be "SYNTH4".');
assert(IP.isIPv4String(json.address),
'Invalid SYNTH4 record. Address must be a valid IPv4 address.');
this.address = IP.normalize(json.address);
return this;
}
}
/**
* SYNTH6
* @extends {Struct}
*/
class SYNTH6 extends Struct {
constructor() {
super();
this.address = '::';
}
get type() {
return hsTypes.SYNTH6;
}
get ns() {
const ip = IP.toBuffer(this.address);
return `_${base32.encodeHex(ip)}._synth.`;
}
getSize() {
return 16;
}
write(bw) {
writeIP(bw, this.address, 16);
return this;
}
read(br) {
this.address = readIP(br, 16);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createAAAA(name, ttl, this.address);
}
getJSON() {
return {
type: 'SYNTH6',
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid SYNTH6 record.');
assert(json.type === 'SYNTH6',
'Invalid SYNTH6 record. Type must be "SYNTH6".');
assert(IP.isIPv6String(json.address),
'Invalid SYNTH6 record. Address must be a valid IPv6 address.');
this.address = IP.normalize(json.address);
return this;
}
}
/**
* TXT
* @extends {Struct}
*/
class TXT extends Struct {
constructor() {
super();
this.txt = [];
}
get type() {
return hsTypes.TXT;
}
getSize() {
let size = 1;
for (const txt of this.txt)
size += sizeString(txt);
return size;
}
write(bw) {
bw.writeU8(this.txt.length);
for (const txt of this.txt)
writeStringBW(bw, txt);
return this;
}
read(br) {
const count = br.readU8();
for (let i = 0; i < count; i++)
this.txt.push(readStringBR(br));
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
const rr = new Record();
const rd = new TXTRecord();
rr.name = name;
rr.type = types.TXT;
rr.ttl = ttl;
rr.data = rd;
rd.txt.push(...this.txt);
return rr;
}
getJSON() {
return {
type: 'TXT',
txt: this.txt
};
}
fromJSON(json) {
assert(json && typeof json === 'object',
'Invalid TXT record.');
assert(json.type === 'TXT',
'Invalid TXT record. Type must be "TXT".');
assert(Array.isArray(json.txt),
'Invalid TXT record. txt must be an Array.');
for (const txt of json.txt) {
assert(typeof txt === 'string',
'Invalid TXT record. Entries in txt Array must be type String.');
assert(txt.length <= 255,
'Invalid TXT record. Entries in txt Array must be <= 255 in length.');
this.txt.push(txt);
}
return this;
}
}
/*
* Helpers
*/
function typeToClass(type) {
assert((type & 0xff) === type);
switch (type) {
case hsTypes.DS:
return DS;
case hsTypes.NS:
return NS;
case hsTypes.GLUE4:
return GLUE4;
case hsTypes.GLUE6:
return GLUE6;
case hsTypes.SYNTH4:
return SYNTH4;
case hsTypes.SYNTH6:
return SYNTH6;
case hsTypes.TXT:
return TXT;
default:
return null;
}
}
function stringToClass(type) {
assert(typeof type === 'string');
if (!Object.prototype.hasOwnProperty.call(hsTypes, type))
return null;
return typeToClass(hsTypes[type]);
}
function createNS(name, ttl, ns) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
assert(util.isFQDN(ns));
const rr = new Record();
const rd = new NSRecord();
rr.name = name;
rr.ttl = ttl;
rr.type = types.NS;
rr.data = rd;
rd.ns = ns;
return rr;
}
function createA(name, ttl, address) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
assert(IP.isIPv4String(address));
const rr = new Record();
const rd = new ARecord();
rr.name = name;
rr.ttl = ttl;
rr.type = types.A;
rr.data = rd;
rd.address = address;
return rr;
}
function createAAAA(name, ttl, address) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
assert(IP.isIPv6String(address));
const rr = new Record();
const rd = new AAAARecord();
rr.name = name;
rr.ttl = ttl;
rr.type = types.AAAA;
rr.data = rd;
rd.address = address;
return rr;
}
/*
* Expose
*/
exports.Resource = Resource;
exports.DS = DS;
exports.NS = NS;
exports.GLUE4 = GLUE4;
exports.GLUE6 = GLUE6;
exports.SYNTH4 = SYNTH4;
exports.SYNTH6 = SYNTH6;
exports.TXT = TXT;