UNPKG

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.

294 lines 10.8 kB
import { parseTtl, getHasValidTtl } from '../ttl/parse_ttl'; import { ParserInvalidArgumentsError } from '../errors/parsing_error_invalid_arguments'; import { getRecordNameUsingOrigin, zipArrayToObj } from './utils_parser'; import { SUPPORTED_RECORD_TYPES, ErrorKeyKind, RecordMinLengthKind, RecordMaxLengthKind, DirectiveKind, } from '../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 = RecordMinLengthKind[recordType]; const maxLength = 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 */ export const assertRecordLengths = (recordType, valuesLength, minLength, maxLength) => { const isExact = +minLength === +maxLength; const isExactAndNotEq = isExact && minLength !== valuesLength; if (isExactAndNotEq || minLength > valuesLength) { throw new ParserInvalidArgumentsError(recordType, valuesLength, minLength); } }; /** * $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); 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 = getHasValidTtl(first); const didSecondHaveTtl = 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] = `${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 = 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; }); }; export const parsers = { NS: zipArrayToObj('host', (record, parser) => ({ ...record, values: fromAtSymbolValuesToOrigin(record, parser), })), A: zipArrayToObj('ip', (record, parser) => ({ ...record, values: fromAtSymbolValuesToOrigin(record, parser), })), AAAA: zipArrayToObj('ip', (record, parser) => ({ ...record, values: fromAtSymbolValuesToOrigin(record, parser), })), CNAME: zipArrayToObj('alias', (record, parser) => ({ ...record, values: fromAtSymbolValuesToOrigin(record, parser), })), MX: zipArrayToObj('preference host', (record, parser) => ({ ...record, values: fromAtSymbolValuesToOrigin(record, parser), })), TXT: zipArrayToObj('txt'), PTR: zipArrayToObj('host', (record, parser) => ({ ...record, values: fromAtSymbolValuesToOrigin(record, parser), })), SRV: zipArrayToObj('target port weight priority'), NAPTR: zipArrayToObj('order preference flags services regexp replacement'), SPF: zipArrayToObj('data', x => ({ ...x, values: [x.values.join(' ').trim()], })), CAA: zipArrayToObj('flags tag data', x => ({ ...x, data: x.data.replace(/^"(.+?)"$/, '$1'), })), SOA: 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: DirectiveKind.Origin, value: lineParts[1], }); }, }; const ttlVisitor = { isSatisfied: ({ lineParts }) => lineParts.indexOf('$TTL') === 0, visit({ zoneObj }, { addMeta, lineParts }) { const [ttl, error] = parseTtl(lineParts[1]); if (!error) { zoneObj.$TTL = ttl; addMeta({ instructionType: DirectiveKind.TimeToLive, value: ttl, }); } else { addMeta({ instructionType: DirectiveKind.TimeToLive, value: ttl, errorType: error, error, }); } }, }; const soaVisitor = { isSatisfied: ({ lineParts }) => lineParts.includes('SOA'), visit({ zoneObj }, { addMeta, lineParts }) { zoneObj.SOA = 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) => 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 = parsers[type](normalized, parser); listForType.push(parsed); addMeta({ instructionType: type, value: parsed }); } catch (error) { addMeta({ instructionType: type, value: lineParts.join(' '), error, errorType: error instanceof ParserInvalidArgumentsError ? ErrorKeyKind.WrongLength : ErrorKeyKind.WrongNameArgsLength, }); } }, }; const fallbackVisitor = { isSatisfied: () => true, visit(parser, { addMeta, lineParts }) { addMeta({ instructionType: ErrorKeyKind.Unknown, errorType: ErrorKeyKind.BadDirective, value: lineParts.join(' '), }); }, }; export const visitors = [ originVisitor, ttlVisitor, soaVisitor, simpleRecordsVisitor, fallbackVisitor, ]; //# sourceMappingURL=visitors.js.map