diff --git a/assets/scripts/Field.js b/assets/scripts/Field.js new file mode 100644 index 0000000..df80f1a --- /dev/null +++ b/assets/scripts/Field.js @@ -0,0 +1,34 @@ +/** + * Defines any key-value pair in any type of DNS-record + * Used for input fields on DNS creator pages + */ +export class Field { + displayName = null; + description = null; + + constructor(key) { + this.key = key; + } + + // Virtual methods + + getInputHtml() { + return null; + } + + getInputValue() { + return null; + } + + // Builder methods + + label(label) { + this.displayName = label; + return this; + } + + desc(description) { + this.description = description; + return this; + } +} diff --git a/assets/scripts/records/DmarcRecord.js b/assets/scripts/records/DmarcRecord.js index 59ece76..326a629 100644 --- a/assets/scripts/records/DmarcRecord.js +++ b/assets/scripts/records/DmarcRecord.js @@ -88,18 +88,4 @@ export class DmarcRecord extends TagListRecord { constructor(text) { super(text); } - - tokenize() { - return this.text - .replace(/;\s*$/, "") - .split(/;\s*/); - } - - fieldsToString() { - let tokens = this.constructor.fields - .filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue) - .filter(field => field.getInputValue()) - .map(field => field.key + "=" + field.getInputValue()); - return tokens.join("; "); - } } diff --git a/assets/scripts/records/SpfRecord.js b/assets/scripts/records/SpfRecord.js index 799880f..a152c31 100644 --- a/assets/scripts/records/SpfRecord.js +++ b/assets/scripts/records/SpfRecord.js @@ -1,53 +1,43 @@ -import { Mechanism } from "../spf/Mechanism.js"; import { ValidationError } from "../ValidationError.js"; import { Modifier } from "../spf/Modifier.js"; import { ValueRequirement } from "../spf/ValueRequirement.js"; +import { DomainMechanism } from "../spf/DomainMechanism.js"; +import { IPv4Mechanism } from "../spf/IPv4Mechanism.js"; +import { IPv6Mechanism } from "../spf/IPv6Mechanism.js"; +import { Mechanism } from "../spf/Mechanism.js"; +import { VersionTerm } from "../spf/VersionTerm.js"; export class SpfRecord { static fields = [ - new Modifier("v") + new VersionTerm("v", "spf1") .required() - .validate((key, val) => { - if (val !== "spf1") throw new ValidationError(`Version must be "spf1"`); - return true; - }) .pos(0), - new Mechanism("include") - .validate(this.validateDomain) + new DomainMechanism("include") .multiple() .pos(1), - new Mechanism("a") - .value(ValueRequirement.OPTIONAL) - .validate(this.validateDomain) + new DomainMechanism("a") .multiple() .pos(1), - new Mechanism("mx") - .value(ValueRequirement.OPTIONAL) - .validate(this.validateDomain) + new DomainMechanism("mx") .multiple() .pos(2), - new Mechanism("ptr") - .value(ValueRequirement.OPTIONAL) - .validate(() => { throw new ValidationError(`"ptr" mechanism should not be used`) }) + new DomainMechanism("ptr") .multiple() .pos(2), - new Mechanism("ipv4") - .validate(this.validateIPv4) + new IPv4Mechanism("ipv4") .multiple() .pos(2), - new Mechanism("ipv6") - .validate(this.validateIPv6) + new IPv6Mechanism("ipv6") .multiple() .pos(2), - new Mechanism("exists") - .validate(this.validateDomain) + new DomainMechanism("exists") .multiple() .pos(2), @@ -56,11 +46,9 @@ export class SpfRecord { .pos(3), new Modifier("redirect") - .validate(this.validateDomain) .pos(4), new Modifier("exp") - .validate(this.validateDomain) .pos(4), ]; @@ -128,40 +116,9 @@ export class SpfRecord { throw new ValidationError(`Term "${term.key}" must not have a value`); } - if (input.value) term.validationFunction(term.key, input.value); + if (input.value) term.validate(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 - } } diff --git a/assets/scripts/records/TagListRecord.js b/assets/scripts/records/TagListRecord.js index 1379ca8..4706cce 100644 --- a/assets/scripts/records/TagListRecord.js +++ b/assets/scripts/records/TagListRecord.js @@ -9,7 +9,9 @@ export class TagListRecord { } tokenize() { - throw new Error("Unimplemented"); + return this.text + .replace(/;\s*$/, "") + .split(/;\s*/); } getKeyValues() { @@ -59,4 +61,12 @@ export class TagListRecord { return true; } + + fieldsToString() { + let tokens = this.constructor.fields + .filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue) + .filter(field => field.getInputValue()) + .map(field => field.key + "=" + field.getInputValue()); + return tokens.join("; "); + } } diff --git a/assets/scripts/spf/DomainMechanism.js b/assets/scripts/spf/DomainMechanism.js new file mode 100644 index 0000000..08f0b5f --- /dev/null +++ b/assets/scripts/spf/DomainMechanism.js @@ -0,0 +1,21 @@ +import { ValidationError } from "../ValidationError.js"; +import { Mechanism } from "./Mechanism.js"; + +export class DomainMechanism extends Mechanism { + separator = ":"; + + constructor(key) { + super(key); + } + + validate(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 "${this.key}" is not a valid domain name`); + + return true; + } +} diff --git a/assets/scripts/spf/IPv4Mechanism.js b/assets/scripts/spf/IPv4Mechanism.js new file mode 100644 index 0000000..e6c3880 --- /dev/null +++ b/assets/scripts/spf/IPv4Mechanism.js @@ -0,0 +1,26 @@ +import { ValidationError } from "../ValidationError.js"; +import { Mechanism } from "./Mechanism.js"; + +export class IPv4Mechanism extends Mechanism { + separator = ":"; + + constructor(key) { + super(key); + } + + validate(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; + } +} diff --git a/assets/scripts/spf/IPv6Mechanism.js b/assets/scripts/spf/IPv6Mechanism.js new file mode 100644 index 0000000..a6c370a --- /dev/null +++ b/assets/scripts/spf/IPv6Mechanism.js @@ -0,0 +1,12 @@ +import { Mechanism } from "./Mechanism.js"; + +export class IPv6Mechanism extends Mechanism { + constructor(key) { + super(key); + } + + validate(value) { + // TODO validate + return true; + } +} diff --git a/assets/scripts/spf/Modifier.js b/assets/scripts/spf/Modifier.js index a64ffe7..639e072 100644 --- a/assets/scripts/spf/Modifier.js +++ b/assets/scripts/spf/Modifier.js @@ -1,4 +1,5 @@ import { Term } from "./Term.js"; +import { ValidationError } from "../ValidationError.js"; export class Modifier extends Term { separator = "="; @@ -6,4 +7,9 @@ export class Modifier extends Term { constructor(key) { super(key); } + + validate() { + // TODO validate + return true; + } } diff --git a/assets/scripts/spf/Term.js b/assets/scripts/spf/Term.js index 5cc81ed..a4a1db0 100644 --- a/assets/scripts/spf/Term.js +++ b/assets/scripts/spf/Term.js @@ -1,15 +1,15 @@ import { ValueRequirement } from "./ValueRequirement.js"; +import { Field } from "../Field.js"; -export class Term { +export class Term extends Field { separator = null; isRequired = false; position = null; allowMultiple = false; valueRequirement = ValueRequirement.REQUIRED; - validationFunction = null; constructor(key) { - this.key = key; + super(key) } // Builder methods @@ -33,9 +33,4 @@ export class Term { this.valueRequirement = requirement; return this; } - - validate(func) { - this.validationFunction = func; - return this; - } } diff --git a/assets/scripts/spf/VersionTerm.js b/assets/scripts/spf/VersionTerm.js new file mode 100644 index 0000000..039220a --- /dev/null +++ b/assets/scripts/spf/VersionTerm.js @@ -0,0 +1,17 @@ +import { Term } from "./Term.js"; +import { ValidationError } from "../ValidationError.js"; + +export class VersionTerm extends Term { + separator = "="; + + constructor(key, version) { + super(key); + this.version = version; + } + + validate(value) { + if (value !== this.version) throw new ValidationError(`Version must be "${this.version}"`); + + return true; + } +} diff --git a/assets/scripts/tags/Tag.js b/assets/scripts/tags/Tag.js index e17cb04..9214b94 100644 --- a/assets/scripts/tags/Tag.js +++ b/assets/scripts/tags/Tag.js @@ -1,13 +1,14 @@ -export class Tag { +import { Field } from "../Field.js"; + +/** A tag within a DMARC/DKIM record */ +export class Tag extends Field { isRequired = false; defaultValue = null; - displayName = null; - description = null; categoryName = null; position = null; constructor(key) { - this.key = key; + super(key); this.id = "tag-" + key; } @@ -17,14 +18,6 @@ export class Tag { return true; } - getInputHtml() { - return null; - } - - getInputValue() { - return null; - } - // Builder methods required() { @@ -37,16 +30,6 @@ export class Tag { return this; } - label(label) { - this.displayName = label; - return this; - } - - desc(description) { - this.description = description; - return this; - } - category(category) { this.categoryName = category; return this;