168 lines
4.0 KiB
JavaScript
168 lines
4.0 KiB
JavaScript
import { Mechanism } from "../spf/Mechanism.js";
|
|
import { ValidationError } from "../ValidationError.js";
|
|
import { Modifier } from "../spf/Modifier.js";
|
|
import { ValueRequirement } from "../spf/ValueRequirement.js";
|
|
|
|
export class SpfRecord {
|
|
static fields = [
|
|
new Modifier("v")
|
|
.required()
|
|
.validate((key, val) => {
|
|
if (val !== "spf1") throw new ValidationError(`Version must be "spf1"`);
|
|
return true;
|
|
})
|
|
.pos(0),
|
|
|
|
new Mechanism("include")
|
|
.validate(this.validateDomain)
|
|
.multiple()
|
|
.pos(1),
|
|
|
|
new Mechanism("a")
|
|
.value(ValueRequirement.OPTIONAL)
|
|
.validate(this.validateDomain)
|
|
.multiple()
|
|
.pos(1),
|
|
|
|
new Mechanism("mx")
|
|
.value(ValueRequirement.OPTIONAL)
|
|
.validate(this.validateDomain)
|
|
.multiple()
|
|
.pos(2),
|
|
|
|
new Mechanism("ptr")
|
|
.value(ValueRequirement.OPTIONAL)
|
|
.validate(() => { throw new ValidationError(`"ptr" mechanism should not be used`) })
|
|
.multiple()
|
|
.pos(2),
|
|
|
|
new Mechanism("ipv4")
|
|
.validate(this.validateIPv4)
|
|
.multiple()
|
|
.pos(2),
|
|
|
|
new Mechanism("ipv6")
|
|
.validate(this.validateIPv6)
|
|
.multiple()
|
|
.pos(2),
|
|
|
|
new Mechanism("exists")
|
|
.validate(this.validateDomain)
|
|
.multiple()
|
|
.pos(2),
|
|
|
|
new Mechanism("all")
|
|
.value(ValueRequirement.PROHIBITED)
|
|
.pos(3),
|
|
|
|
new Modifier("redirect")
|
|
.validate(this.validateDomain)
|
|
.pos(4),
|
|
|
|
new Modifier("exp")
|
|
.validate(this.validateDomain)
|
|
.pos(4),
|
|
];
|
|
|
|
constructor(text) {
|
|
this.text = text;
|
|
}
|
|
|
|
tokenize() {
|
|
return this.text.trim().split(/\s+/);
|
|
}
|
|
|
|
getKeyValues() {
|
|
const result = [];
|
|
|
|
for (const token of this.tokenize()) {
|
|
const name = token.match(/^[-+?~]?(\w*)/)[1];
|
|
|
|
const term = this.constructor.fields.find(f => f.key === name);
|
|
if (!term) {
|
|
throw new ValidationError(`Unknown term: ${name}`);
|
|
}
|
|
|
|
const [directive, value] = token.split(term.separator);
|
|
const [, qualifier, key] = directive.match(/^([-+?~]?)(\w*)/);
|
|
|
|
result.push({ qualifier, key, value });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
validate() {
|
|
const values = this.getKeyValues();
|
|
|
|
for (const term of this.constructor.fields) {
|
|
if (term.isRequired && !values.some(v => v.key === term.key)) {
|
|
throw new ValidationError(`Term "${term.key}" is required`);
|
|
}
|
|
}
|
|
|
|
let lastPos = 0;
|
|
for (let i = 0; i < values.length; i++) {
|
|
const input = values[i];
|
|
const term = this.constructor.fields.find(d => d.key === input.key);
|
|
|
|
if (!term) {
|
|
throw new ValidationError(`Unknown term: ${input.key}`);
|
|
}
|
|
|
|
if (term.position < lastPos) {
|
|
const lastDirective = this.constructor.fields.find(d => d.key === values[i-1].key);
|
|
|
|
throw new ValidationError(`Term "${lastDirective.key}" must come after "${term.key}"`);
|
|
}
|
|
|
|
if (term instanceof Modifier && input.qualifier) {
|
|
throw new ValidationError(`Modifier "${term.key}" must not have a qualifier`)
|
|
}
|
|
|
|
if (!input.value && term.valueRequirement === ValueRequirement.REQUIRED) {
|
|
throw new ValidationError(`Term "${term.key}" is missing a value`);
|
|
}
|
|
|
|
if (input.value && term.valueRequirement === ValueRequirement.PROHIBITED) {
|
|
throw new ValidationError(`Term "${term.key}" must not have a value`);
|
|
}
|
|
|
|
if (input.value) term.validationFunction(term.key, input.value);
|
|
|
|
lastPos = term.position;
|
|
}
|
|
}
|
|
|
|
static validateDomain(key, value) {
|
|
// https://www.rfc-editor.org/rfc/rfc7208#section-7.1
|
|
const valid = value
|
|
.split(".")
|
|
.every(segment => segment.match(/^([^%]|%_|%%|%-|%\{[slodiphcrtv]\d*r?[-.+,\/_=]*})+$/));
|
|
|
|
if (!valid) throw new ValidationError(`Value for "${key}" is not a valid domain name`);
|
|
|
|
return true;
|
|
}
|
|
|
|
static validateIPv4(key, value) {
|
|
const segments = value.split(".");
|
|
|
|
const valid = segments.every(segment => {
|
|
const number = parseInt(segment);
|
|
|
|
return !isNaN(number) && number >= 0 && number <= 255;
|
|
});
|
|
|
|
if (segments.length !== 4 || !valid) {
|
|
throw new ValidationError(`Value for ${key} is not a valid IPv4 address`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static validateIPv6(key, value) {
|
|
return true; // TODO validate properly
|
|
}
|
|
}
|