Extract mechanisms, create common field class

This commit is contained in:
Reimar 2026-01-15 10:59:37 +01:00
parent 80ad069793
commit 00c8a999e7
Signed by: Reimar
GPG Key ID: 93549FA07F0AE268
11 changed files with 149 additions and 102 deletions

34
assets/scripts/Field.js Normal file
View File

@ -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;
}
}

View File

@ -88,18 +88,4 @@ export class DmarcRecord extends TagListRecord {
constructor(text) { constructor(text) {
super(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("; ");
}
} }

View File

@ -1,53 +1,43 @@
import { Mechanism } from "../spf/Mechanism.js";
import { ValidationError } from "../ValidationError.js"; import { ValidationError } from "../ValidationError.js";
import { Modifier } from "../spf/Modifier.js"; import { Modifier } from "../spf/Modifier.js";
import { ValueRequirement } from "../spf/ValueRequirement.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 { export class SpfRecord {
static fields = [ static fields = [
new Modifier("v") new VersionTerm("v", "spf1")
.required() .required()
.validate((key, val) => {
if (val !== "spf1") throw new ValidationError(`Version must be "spf1"`);
return true;
})
.pos(0), .pos(0),
new Mechanism("include") new DomainMechanism("include")
.validate(this.validateDomain)
.multiple() .multiple()
.pos(1), .pos(1),
new Mechanism("a") new DomainMechanism("a")
.value(ValueRequirement.OPTIONAL)
.validate(this.validateDomain)
.multiple() .multiple()
.pos(1), .pos(1),
new Mechanism("mx") new DomainMechanism("mx")
.value(ValueRequirement.OPTIONAL)
.validate(this.validateDomain)
.multiple() .multiple()
.pos(2), .pos(2),
new Mechanism("ptr") new DomainMechanism("ptr")
.value(ValueRequirement.OPTIONAL)
.validate(() => { throw new ValidationError(`"ptr" mechanism should not be used`) })
.multiple() .multiple()
.pos(2), .pos(2),
new Mechanism("ipv4") new IPv4Mechanism("ipv4")
.validate(this.validateIPv4)
.multiple() .multiple()
.pos(2), .pos(2),
new Mechanism("ipv6") new IPv6Mechanism("ipv6")
.validate(this.validateIPv6)
.multiple() .multiple()
.pos(2), .pos(2),
new Mechanism("exists") new DomainMechanism("exists")
.validate(this.validateDomain)
.multiple() .multiple()
.pos(2), .pos(2),
@ -56,11 +46,9 @@ export class SpfRecord {
.pos(3), .pos(3),
new Modifier("redirect") new Modifier("redirect")
.validate(this.validateDomain)
.pos(4), .pos(4),
new Modifier("exp") new Modifier("exp")
.validate(this.validateDomain)
.pos(4), .pos(4),
]; ];
@ -128,40 +116,9 @@ export class SpfRecord {
throw new ValidationError(`Term "${term.key}" must not have a value`); 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; 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
}
} }

View File

@ -9,7 +9,9 @@ export class TagListRecord {
} }
tokenize() { tokenize() {
throw new Error("Unimplemented"); return this.text
.replace(/;\s*$/, "")
.split(/;\s*/);
} }
getKeyValues() { getKeyValues() {
@ -59,4 +61,12 @@ export class TagListRecord {
return true; 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("; ");
}
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,12 @@
import { Mechanism } from "./Mechanism.js";
export class IPv6Mechanism extends Mechanism {
constructor(key) {
super(key);
}
validate(value) {
// TODO validate
return true;
}
}

View File

@ -1,4 +1,5 @@
import { Term } from "./Term.js"; import { Term } from "./Term.js";
import { ValidationError } from "../ValidationError.js";
export class Modifier extends Term { export class Modifier extends Term {
separator = "="; separator = "=";
@ -6,4 +7,9 @@ export class Modifier extends Term {
constructor(key) { constructor(key) {
super(key); super(key);
} }
validate() {
// TODO validate
return true;
}
} }

View File

@ -1,15 +1,15 @@
import { ValueRequirement } from "./ValueRequirement.js"; import { ValueRequirement } from "./ValueRequirement.js";
import { Field } from "../Field.js";
export class Term { export class Term extends Field {
separator = null; separator = null;
isRequired = false; isRequired = false;
position = null; position = null;
allowMultiple = false; allowMultiple = false;
valueRequirement = ValueRequirement.REQUIRED; valueRequirement = ValueRequirement.REQUIRED;
validationFunction = null;
constructor(key) { constructor(key) {
this.key = key; super(key)
} }
// Builder methods // Builder methods
@ -33,9 +33,4 @@ export class Term {
this.valueRequirement = requirement; this.valueRequirement = requirement;
return this; return this;
} }
validate(func) {
this.validationFunction = func;
return this;
}
} }

View File

@ -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;
}
}

View File

@ -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; isRequired = false;
defaultValue = null; defaultValue = null;
displayName = null;
description = null;
categoryName = null; categoryName = null;
position = null; position = null;
constructor(key) { constructor(key) {
this.key = key; super(key);
this.id = "tag-" + key; this.id = "tag-" + key;
} }
@ -17,14 +18,6 @@ export class Tag {
return true; return true;
} }
getInputHtml() {
return null;
}
getInputValue() {
return null;
}
// Builder methods // Builder methods
required() { required() {
@ -37,16 +30,6 @@ export class Tag {
return this; return this;
} }
label(label) {
this.displayName = label;
return this;
}
desc(description) {
this.description = description;
return this;
}
category(category) { category(category) {
this.categoryName = category; this.categoryName = category;
return this; return this;