amazon-route-53-dns-zone-file
Version:
Makes DNS Zone File easy. Parses and validates BIND zone files and can be extended for custom features. Functionality is modular. Features are made open for extension and closed for runtime mutation. Written in TypeScript.
298 lines • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.visitors = exports.parsers = exports.assertRecordLengths = void 0;
const parse_ttl_1 = require("../ttl/parse_ttl");
const parsing_error_invalid_arguments_1 = require("../errors/parsing_error_invalid_arguments");
const utils_parser_1 = require("./utils_parser");
const constants_domain_specific_1 = require("../shared/constants_domain_specific");
/** we indexed how long each value must be, so subtract $NAME_TTL + $IN_TYPE + $VALUES */
const getLengths = (recordType, values, lineParts) => {
const valuesLength = values.length;
const minLength = constants_domain_specific_1.RecordMinLengthKind[recordType];
const maxLength = constants_domain_specific_1.RecordMaxLengthKind[recordType];
const nameAndTtlLength = lineParts.length - valuesLength - 2;
return [nameAndTtlLength, valuesLength, minLength, maxLength];
};
/**
* @idea we can include this in validation so it can be extended
* @note this uses Lengths type (everything after nameAndTtl)
* @throws {ParserInvalidArgumentsError} with invalid input lengths
*/
const assertRecordLengths = (recordType, valuesLength, minLength, maxLength) => {
const isExact = +minLength === +maxLength;
const isExactAndNotEq = isExact && minLength !== valuesLength;
if (isExactAndNotEq || minLength > valuesLength) {
throw new parsing_error_invalid_arguments_1.ParserInvalidArgumentsError(recordType, valuesLength, minLength);
}
};
exports.assertRecordLengths = assertRecordLengths;
/**
* $NAME_TTL = [name] [ttl]
* $IN_TYPE = `IN $TYPE`
*
* [name] [ttl] - IN $TYPE - value < 4
* [ttl] - IN $TYPE - value < 3
* - IN $TYPE - value < 2
*
* [ttl] [name] - IN TYPE - value < this is not supported by prod
*
* these 2 are where it gets tricky - do we prefer name, or ttl?
* [name] - IN TYPE - value
* [ttl] - IN TYPE - value
*
* @perf bench & experiment as a class
* @note has sideEffects - may swap ttl order in array
*/
const normalizeRecord = (lineContent, lineParts, recordType, zoneObj, previousName) => {
/** add `IN` if it does not exist */
const typeIndex = lineParts.lastIndexOf(recordType);
let newName = '';
if (typeIndex === 0 || lineParts[typeIndex - 1] !== 'IN') {
lineParts.splice(typeIndex, 0, 'IN');
}
/** remove duplicate `IN` */
const firstInIndex = lineParts.indexOf('IN');
const lastInIndex = lineParts.lastIndexOf('IN');
if (firstInIndex !== lastInIndex) {
lineParts.splice(firstInIndex, 1);
}
const values = lineParts.slice(lineParts.lastIndexOf(recordType) + 1);
const [nameAndTtlLength, ...rest] = getLengths(recordType, values, lineParts);
exports.assertRecordLengths(recordType, ...rest);
/**
* @todo @perf move to separate fn
* @spec RFC-1035: <lineContent> contents are oneOf:
* @example
* `[<TTL>] [<class>] <type> <RDATA>` // assume this one
* `[<class>] [<TTL>] <type> <RDATA>` // but also support this one
*/
const getHas = () => {
switch (nameAndTtlLength) {
// we have name || ttl
case 1: {
// does not start with a space (if so, then it's a child of the parent [as long as parent is same type])
const hasName = /^\s+/.test(lineContent) === false;
return {
hasTtl: !hasName,
hasName,
};
}
// ([name, ttl] | [ttl, name]) => [name, ttl]
case 2: {
const [first, second] = lineParts;
const didFirstHaveTtl = parse_ttl_1.getHasValidTtl(first);
const didSecondHaveTtl = parse_ttl_1.getHasValidTtl(second);
const hasTtl = didFirstHaveTtl || didSecondHaveTtl;
// swap order
if (didFirstHaveTtl && !didSecondHaveTtl) {
lineParts[0] = second;
lineParts[1] = first;
}
return {
hasTtl,
hasName: lineParts[0] !== '',
};
}
// no name or ttl, we can use the top level
case 0: {
return {
hasTtl: false,
hasName: false,
};
}
// too many args for name + ttl
default: {
throw new Error(recordType);
}
}
};
const { hasName, hasTtl } = getHas();
const normalized = {
values,
recordType,
tokens: lineParts,
};
// unshift name
if (!hasName) {
const recordsSoFar = zoneObj[recordType];
/** @idea simplify by removing the else if here since we have previousName now */
if (previousName) {
normalized.tokens.unshift(previousName);
}
else if (Array.isArray(recordsSoFar) && recordsSoFar.length > 0) {
normalized.tokens.unshift(recordsSoFar[recordsSoFar.length - 1].name || '@');
}
else {
normalized.tokens.unshift('@');
}
}
else {
newName = normalized.tokens[0];
}
// unshift ttl
if (hasTtl) {
normalized.tokens[1] = `${parse_ttl_1.parseTtl(normalized.tokens[1])[0]}`;
}
else {
// move name to 0, then put ttl at 1
normalized.tokens.unshift(normalized.tokens[0]);
// we want the tokens to stay as strings, so we cast the numerical $ttl to a string
// if there is not a ttl, we have added an `undefined` to this array
normalized.tokens[1] =
zoneObj.$TTL === undefined ? undefined : `${zoneObj.$TTL}`;
}
const [nameValue, ttlValue] = normalized.tokens;
const finalName = utils_parser_1.getRecordNameUsingOrigin(nameValue, zoneObj);
return {
...normalized,
name: finalName,
ttl: typeof ttlValue === 'string' ? +ttlValue : undefined,
previousName: newName || previousName,
};
};
const fromAtSymbolValuesToOrigin = ({ values }, { zoneObj }) => {
return values.map(x => {
// replace @ with origin
if (x === '@' && zoneObj.$ORIGIN)
return zoneObj.$ORIGIN;
// escaped values
else if (x === '\\@')
return '@';
// nothing changed
return x;
});
};
exports.parsers = {
NS: utils_parser_1.zipArrayToObj('host', (record, parser) => ({
...record,
values: fromAtSymbolValuesToOrigin(record, parser),
})),
A: utils_parser_1.zipArrayToObj('ip', (record, parser) => ({
...record,
values: fromAtSymbolValuesToOrigin(record, parser),
})),
AAAA: utils_parser_1.zipArrayToObj('ip', (record, parser) => ({
...record,
values: fromAtSymbolValuesToOrigin(record, parser),
})),
CNAME: utils_parser_1.zipArrayToObj('alias', (record, parser) => ({
...record,
values: fromAtSymbolValuesToOrigin(record, parser),
})),
MX: utils_parser_1.zipArrayToObj('preference host', (record, parser) => ({
...record,
values: fromAtSymbolValuesToOrigin(record, parser),
})),
TXT: utils_parser_1.zipArrayToObj('txt'),
PTR: utils_parser_1.zipArrayToObj('host', (record, parser) => ({
...record,
values: fromAtSymbolValuesToOrigin(record, parser),
})),
SRV: utils_parser_1.zipArrayToObj('target port weight priority'),
NAPTR: utils_parser_1.zipArrayToObj('order preference flags services regexp replacement'),
SPF: utils_parser_1.zipArrayToObj('data', x => ({
...x,
values: [x.values.join(' ').trim()],
})),
CAA: utils_parser_1.zipArrayToObj('flags tag data', x => ({
...x,
data: x.data.replace(/^"(.+?)"$/, '$1'),
})),
SOA: utils_parser_1.zipArrayToObj('minimum expire retry refresh serial rname mname'),
};
const originVisitor = {
isSatisfied: ({ lineParts }) => lineParts.indexOf('$ORIGIN') === 0,
visit(parser, { lineParts, addMeta }) {
parser.zoneObj.$ORIGIN = lineParts[1];
return addMeta({
instructionType: constants_domain_specific_1.DirectiveKind.Origin,
value: lineParts[1],
});
},
};
const ttlVisitor = {
isSatisfied: ({ lineParts }) => lineParts.indexOf('$TTL') === 0,
visit({ zoneObj }, { addMeta, lineParts }) {
const [ttl, error] = parse_ttl_1.parseTtl(lineParts[1]);
if (!error) {
zoneObj.$TTL = ttl;
addMeta({
instructionType: constants_domain_specific_1.DirectiveKind.TimeToLive,
value: ttl,
});
}
else {
addMeta({
instructionType: constants_domain_specific_1.DirectiveKind.TimeToLive,
value: ttl,
errorType: error,
error,
});
}
},
};
const soaVisitor = {
isSatisfied: ({ lineParts }) => lineParts.includes('SOA'),
visit({ zoneObj }, { addMeta, lineParts }) {
zoneObj.SOA = exports.parsers.SOA({
tokens: lineParts,
values: lineParts.slice(lineParts.indexOf('SOA') + 1),
});
addMeta({ instructionType: 'SOA', value: zoneObj.SOA });
},
};
/**
* intersection.
* checks whether the array of strings include any of the supported record types
*/
const getSupportedType = (lineParts) => constants_domain_specific_1.SUPPORTED_RECORD_TYPES.find(supportedType => lineParts.includes(supportedType));
const simpleRecordsVisitor = {
isSatisfied: ({ lineParts }) => !!getSupportedType(lineParts),
visit(parser, { addMeta, lineParts, lineContent }) {
var _a, _b;
const { zoneObj, stack } = parser;
const type = getSupportedType(lineParts);
// make sure the list exists
if (!Array.isArray(zoneObj[type])) {
zoneObj[type] = [];
}
// reference the array and because we defaulted it above, assert the type
const listForType = zoneObj[type];
try {
const previousName = (_b = (_a = stack[stack.length - 1]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '';
const normalized = normalizeRecord(lineContent, lineParts, type, zoneObj, previousName);
stack.push(normalized);
const parsed = exports.parsers[type](normalized, parser);
listForType.push(parsed);
addMeta({ instructionType: type, value: parsed });
}
catch (error) {
addMeta({
instructionType: type,
value: lineParts.join(' '),
error,
errorType: error instanceof parsing_error_invalid_arguments_1.ParserInvalidArgumentsError
? constants_domain_specific_1.ErrorKeyKind.WrongLength
: constants_domain_specific_1.ErrorKeyKind.WrongNameArgsLength,
});
}
},
};
const fallbackVisitor = {
isSatisfied: () => true,
visit(parser, { addMeta, lineParts }) {
addMeta({
instructionType: constants_domain_specific_1.ErrorKeyKind.Unknown,
errorType: constants_domain_specific_1.ErrorKeyKind.BadDirective,
value: lineParts.join(' '),
});
},
};
exports.visitors = [
originVisitor,
ttlVisitor,
soaVisitor,
simpleRecordsVisitor,
fallbackVisitor,
];
//# sourceMappingURL=visitors.js.map