DNS checks read a long TXT record the server splits across lines #64
No files matched your search
@@ -54,6 +54,27 @@ describe('parseZone', () => {
|
||||
expect(records.find((r) => r.kind === 'dkim')?.value).toBe('"v=DKIM1; k=ed25519; p=abc"');
|
||||
});
|
||||
|
||||
it('joins a long TXT record the server splits across lines, as BIND does', () => {
|
||||
const zone = [
|
||||
'example.com. IN MX 10 mail.example.com.',
|
||||
'rsa._domainkey.example.com. IN TXT (',
|
||||
' "v=DKIM1; k=rsa; p=MIIBIjAN"',
|
||||
' "BgkqhkiG9w0BAQEFAAOC"',
|
||||
')',
|
||||
'_dmarc.example.com. IN TXT "v=DMARC1; p=reject"',
|
||||
].join('\n');
|
||||
const records = parseZone(zone);
|
||||
const dkim = records.find((r) => r.kind === 'dkim');
|
||||
expect(dkim?.value).toBe('"v=DKIM1; k=rsa; p=MIIBIjAN" "BgkqhkiG9w0BAQEFAAOC"');
|
||||
expect(normalizeValue('TXT', dkim!.value)).toBe(normalizeValue('TXT', '"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC"'));
|
||||
expect(records.map((r) => r.kind)).toEqual(['mx', 'dkim', 'dmarc']);
|
||||
});
|
||||
|
||||
it('keeps parentheses that are inside a quoted value', () => {
|
||||
const [r] = parseZone('_dmarc.example.com. IN TXT "v=DMARC1; p=none; (test)"');
|
||||
expect(r.value).toBe('"v=DMARC1; p=none; (test)"');
|
||||
});
|
||||
|
||||
it('copes with nothing', () => {
|
||||
expect(parseZone(undefined)).toEqual([]);
|
||||
expect(parseZone('')).toEqual([]);
|
||||
|
||||
@@ -67,10 +67,62 @@ function kindOf(name: string, type: string, value: string): RecordKind | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The text with the quoted strings blanked, so parentheses inside them don't count. */
|
||||
function unquoted(line: string): string {
|
||||
return line.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the lines a record spans. The server writes a TXT value longer than
|
||||
* 255 bytes the way BIND does, as quoted chunks inside parentheses on lines
|
||||
* of their own:
|
||||
*
|
||||
* sel._domainkey.example.com. IN TXT (
|
||||
* "v=DKIM1; k=rsa; p=MIIB…"
|
||||
* "…IDAQAB"
|
||||
* )
|
||||
*
|
||||
* Read a line at a time, the record was "(" and the DKIM key never matched
|
||||
* what DNS returns, so every domain with an RSA key showed as missing it.
|
||||
*/
|
||||
function logicalLines(text: string): string[] {
|
||||
const out: string[] = [];
|
||||
let pending: string | null = null;
|
||||
for (const raw of text.split('\n')) {
|
||||
const line = raw.trim();
|
||||
if (pending !== null) {
|
||||
pending += ` ${line}`;
|
||||
if (unquoted(line).includes(')')) {
|
||||
out.push(pending);
|
||||
pending = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const bare = unquoted(line);
|
||||
if (bare.includes('(') && !bare.includes(')')) pending = line;
|
||||
else out.push(line);
|
||||
}
|
||||
if (pending !== null) out.push(pending);
|
||||
// The parentheses only group lines; the record doesn't contain them.
|
||||
return out.map((l) => {
|
||||
const bare = unquoted(l);
|
||||
if (!bare.includes('(')) return l;
|
||||
let result = '';
|
||||
let inQuote = false;
|
||||
for (let i = 0; i < l.length; i++) {
|
||||
const c = l[i];
|
||||
if (c === '"' && l[i - 1] !== '\\') inQuote = !inQuote;
|
||||
if (!inQuote && (c === '(' || c === ')')) continue;
|
||||
result += c;
|
||||
}
|
||||
return result.replace(/\s+/g, ' ').trim();
|
||||
});
|
||||
}
|
||||
|
||||
/** Parse the zone text. Lines it can't place are left out, not guessed at. */
|
||||
export function parseZone(text: string | null | undefined): ZoneRecord[] {
|
||||
const out: ZoneRecord[] = [];
|
||||
for (const raw of (text ?? '').split('\n')) {
|
||||
for (const raw of logicalLines(text ?? '')) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith(';')) continue;
|
||||
const f = fields(line);
|
||||
|
||||
Reference in new issue
Block a user