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) {
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 { 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
}
}

View File

@ -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("; ");
}
}

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

View File

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

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