WIP add SPF validator
This commit is contained in:
parent
75cf0abb1a
commit
82c4579c58
@ -41,6 +41,11 @@ function generate() {
|
||||
document.getElementById("record").value = tool.fieldsToString();
|
||||
}
|
||||
|
||||
document.getElementById("record").onclick = (e) => {
|
||||
e.target.select();
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
function isUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class ConstantField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key, value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
|
||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class DmarcUriListField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
}
|
||||
@ -10,7 +12,7 @@ export class DmarcUriListField extends Field {
|
||||
const uris = value.split(",");
|
||||
|
||||
for (let uri of uris) {
|
||||
uri = uri.replace(/!\d+[kmgt]$/);
|
||||
uri = uri.replace(/!\d+[kmgt]?$/);
|
||||
|
||||
try {
|
||||
new URL(uri);
|
||||
|
||||
17
assets/scripts/fields/DomainField.js
Normal file
17
assets/scripts/fields/DomainField.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class DomainField extends Field {
|
||||
constructor(key, separator) {
|
||||
super(key);
|
||||
this.separator = separator;
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
if (!value.match(/\w+(\.\w+)+/)) {
|
||||
throw new ValidationError(`Field ${this.key} is not a valid domain`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class EnumField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key, values) {
|
||||
super(key);
|
||||
this.values = values;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
export class Field {
|
||||
separator = null;
|
||||
isRequired = false;
|
||||
allowMultiple = false;
|
||||
defaultValue = null;
|
||||
displayName = null;
|
||||
description = null;
|
||||
categoryName = null;
|
||||
requiredIndex = null;
|
||||
afterFieldName = null;
|
||||
position = null;
|
||||
|
||||
constructor(key) {
|
||||
this.key = key;
|
||||
@ -33,6 +34,11 @@ export class Field {
|
||||
return this;
|
||||
}
|
||||
|
||||
multiple() {
|
||||
this.allowMultiple = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
default(value) {
|
||||
this.defaultValue = value;
|
||||
return this;
|
||||
@ -53,13 +59,8 @@ export class Field {
|
||||
return this;
|
||||
}
|
||||
|
||||
atIndex(i) {
|
||||
this.requiredIndex = i;
|
||||
return this;
|
||||
}
|
||||
|
||||
after(field) {
|
||||
this.afterFieldName = field;
|
||||
pos(i) {
|
||||
this.position = i;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class IntField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key, min, max) {
|
||||
super(key);
|
||||
this.min = min;
|
||||
|
||||
@ -7,47 +7,53 @@ import { DnsTool } from "./DnsTool.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class DmarcTool extends DnsTool {
|
||||
static allowWhitespaceAroundSeparator = true;
|
||||
|
||||
static fields = [
|
||||
new ConstantField("v", "DMARC1")
|
||||
.required()
|
||||
.atIndex(0),
|
||||
.pos(0),
|
||||
|
||||
new EnumField("p", ["none", "quarantine", "reject"])
|
||||
.label("Mail Receiver policy")
|
||||
.desc("How to handle failed validations. The email may be quarantined (usually means sent to spam) or rejected")
|
||||
.options(["None", "Quarantine", "Reject"])
|
||||
.required()
|
||||
.atIndex(1)
|
||||
.after("v"),
|
||||
.pos(1),
|
||||
|
||||
new EnumField("adkim", ["r", "s"])
|
||||
.label("DKIM")
|
||||
.desc("How strictly to handle DKIM validation")
|
||||
.options(["Relaxed", "Strict"])
|
||||
.default("r"),
|
||||
.default("r")
|
||||
.pos(2),
|
||||
|
||||
new EnumField("aspf", ["r", "s"])
|
||||
.label("SPF")
|
||||
.desc("How strictly to handle SPF validation")
|
||||
.options(["Relaxed", "Strict"])
|
||||
.default("r"),
|
||||
.default("r")
|
||||
.pos(2),
|
||||
|
||||
new EnumField("sp", ["none", "quarantine", "reject"])
|
||||
.label("Mail Receiver policy (for subdomains)")
|
||||
.desc("Same as Mail Receiver policy, but applies only to subdomains. If not set, Mail Receiver policy applies to both top-level domain and subdomains")
|
||||
.category("advanced")
|
||||
.options(["None", "Quarantine", "Reject"]),
|
||||
.options(["None", "Quarantine", "Reject"])
|
||||
.pos(2),
|
||||
|
||||
new IntField("pct", 0, 100)
|
||||
.label("Percentage")
|
||||
.desc("Percentage of emails to apply DMARC validation on. Useful for split-testing and continuous rollout")
|
||||
.category("advanced")
|
||||
.default(100),
|
||||
.default(100)
|
||||
.pos(2),
|
||||
|
||||
new DmarcUriListField("ruf")
|
||||
.label("Send failure reports to")
|
||||
.desc("When DMARC validation fails, reports are sent to this email")
|
||||
.category("failure-reporting"),
|
||||
.category("failure-reporting")
|
||||
.pos(2),
|
||||
|
||||
new EnumField("fo", ["0", "1", "d", "s"])
|
||||
.label("Failure reporting options")
|
||||
@ -59,7 +65,8 @@ export class DmarcTool extends DnsTool {
|
||||
"Generate DKIM failure report",
|
||||
"Generate SPF failure report",
|
||||
])
|
||||
.default("0"),
|
||||
.default("0")
|
||||
.pos(2),
|
||||
|
||||
new DmarcUriListField("rua")
|
||||
.label("Send aggregate feedback to")
|
||||
@ -70,7 +77,8 @@ export class DmarcTool extends DnsTool {
|
||||
.label("Aggregate report interval")
|
||||
.desc("Interval (in seconds) between aggregate reports")
|
||||
.category("failure-reporting")
|
||||
.default(86400),
|
||||
.default(86400)
|
||||
.pos(2),
|
||||
|
||||
new Field("rf").default("afrf"), // Other values not supported
|
||||
];
|
||||
@ -86,26 +94,10 @@ export class DmarcTool extends DnsTool {
|
||||
|
||||
tokenize() {
|
||||
return this.text
|
||||
.replace(/;\s+$/, "")
|
||||
.replace(/;\s*$/, "")
|
||||
.split(/;\s*/);
|
||||
}
|
||||
|
||||
getKeyValues() {
|
||||
const result = [];
|
||||
|
||||
for (const token of this.tokenize()) {
|
||||
const [key, value] = token.split(/\s*=\s*/);
|
||||
|
||||
if (!value) {
|
||||
throw new ValidationError(`Field "${key}" is missing a value`);
|
||||
}
|
||||
|
||||
result.push({ key, value });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fieldsToString() {
|
||||
let tokens = this.constructor.fields
|
||||
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
|
||||
|
||||
@ -1,44 +1,70 @@
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class DnsTool {
|
||||
static allowWhitespaceAroundSeparator;
|
||||
static fields = [];
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getKeyValues() {
|
||||
tokenize() {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
getKeyValues() {
|
||||
const result = [];
|
||||
|
||||
for (const token of this.tokenize()) {
|
||||
const key = token.match(/^\w*/)[0];
|
||||
|
||||
const field = this.constructor.fields.find(f => f.key === key);
|
||||
if (!field) {
|
||||
throw new ValidationError(`Unknown field: ${key}`);
|
||||
}
|
||||
|
||||
const wsp = this.constructor.allowWhitespaceAroundSeparator ? "\\s*" : "";
|
||||
const separator = new RegExp(wsp + field.separator + wsp);
|
||||
|
||||
const value = token.split(separator)[1];
|
||||
|
||||
if (!value) {
|
||||
throw new ValidationError(`Field "${key}" is missing a value`);
|
||||
}
|
||||
|
||||
result.push({ key, value });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const values = this.getKeyValues();
|
||||
|
||||
// Validate field order
|
||||
for (const field of this.constructor.fields) {
|
||||
const valueIdx = values.findIndex(v => v.key === field.key);
|
||||
|
||||
if (field.isRequired && valueIdx === -1) {
|
||||
if (field.isRequired && !values.some(v => v.key === field.key)) {
|
||||
throw new ValidationError(`Field "${field.key}" is required`);
|
||||
}
|
||||
|
||||
if (field.requiredIndex && field.requiredIndex !== valueIdx) {
|
||||
if (field.requiredIndex === 0) throw new ValidationError(`Field "${field.key}" must come first`);
|
||||
if (field.afterFieldName) throw new ValidationError(`Field "${field.key}" must come after "${field.afterFieldName}"`);
|
||||
throw new ValidationError(`Field "${field.key}" must be at position ${field.requiredIndex + 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate field values
|
||||
let lastPos = 0;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const input = values[i];
|
||||
const field = this.constructor.fields.find(f => f.key === input.key);
|
||||
|
||||
if (!field) {
|
||||
throw new ValidationError(`Unknown field: ${input.key}`)
|
||||
throw new ValidationError(`Unknown field: ${input.key}`);
|
||||
}
|
||||
|
||||
if (field.position < lastPos) {
|
||||
const lastField = this.constructor.fields.find(f => f.key === values[i-1].key);
|
||||
|
||||
throw new ValidationError(`Field "${lastField.key}" must come after "${field.key}"`);
|
||||
}
|
||||
|
||||
field.validate(input.value);
|
||||
|
||||
lastPos = field.position;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
57
assets/scripts/tools/SpfTool.js
Normal file
57
assets/scripts/tools/SpfTool.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { ConstantField } from "../fields/ConstantField.js";
|
||||
import { DomainField } from "../fields/DomainField.js";
|
||||
import { DnsTool } from "./DnsTool.js";
|
||||
|
||||
export class SpfTool extends DnsTool {
|
||||
static allowWhitespaceAroundSeparator = false;
|
||||
|
||||
static fields = [
|
||||
new ConstantField("v", "spf1")
|
||||
.required()
|
||||
.pos(0),
|
||||
|
||||
new DomainField("include", ":")
|
||||
.multiple()
|
||||
.pos(1),
|
||||
|
||||
new DomainField("a", ":")
|
||||
.multiple()
|
||||
.pos(1),
|
||||
|
||||
new DomainField("mx", ":")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainField("ptr", ":")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainField("ipv4", ":")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainField("ipv6", ":")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainField("exists", ":")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainField("redirect", "=")
|
||||
.pos(3),
|
||||
|
||||
new DomainField("exp", "=")
|
||||
.pos(3),
|
||||
|
||||
// TODO all
|
||||
];
|
||||
|
||||
constructor(text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
tokenize() {
|
||||
return this.text.split(/\s+/);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import { DmarcTool } from "./tools/DmarcTool.js";
|
||||
import { SpfTool } from "./tools/SpfTool.js";
|
||||
|
||||
const tools = {
|
||||
"/dmarc-validator": DmarcTool,
|
||||
"/spf-validator": SpfTool,
|
||||
};
|
||||
|
||||
const Tool = tools[location.pathname];
|
||||
|
||||
@ -45,11 +45,7 @@ label {
|
||||
transition: all 200ms;
|
||||
}
|
||||
|
||||
#record:disabled {
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
#record:not(:disabled):hover {
|
||||
#record:hover {
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<h1>DMARC Record Creator</h1>
|
||||
|
||||
<label for="record">DNS Record</label><br>
|
||||
<input id="record" type="text" disabled>
|
||||
<input id="record" type="text" readonly>
|
||||
|
||||
<main>
|
||||
<h2>Create a DMARC DNS Record</h2>
|
||||
|
||||
38
spf-validator/index.html
Normal file
38
spf-validator/index.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SPF Record Validator - Validate SPF DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/validator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SPF Record Validator</h1>
|
||||
|
||||
<label for="record">DNS Record</label><br>
|
||||
<input id="record" type="text" placeholder="v=spf1 ..." autocapitalize="off" spellcheck="false">
|
||||
|
||||
<main>
|
||||
<h2>Paste an SPF DNS record into the input field to check if it is valid.</h2>
|
||||
|
||||
<p id="error" class="validation-result">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM231 231C240.4 221.6 255.6 221.6 264.9 231L319.9 286L374.9 231C384.3 221.6 399.5 221.6 408.8 231C418.1 240.4 418.2 255.6 408.8 264.9L353.8 319.9L408.8 374.9C418.2 384.3 418.2 399.5 408.8 408.8C399.4 418.1 384.2 418.2 374.9 408.8L319.9 353.8L264.9 408.8C255.5 418.2 240.3 418.2 231 408.8C221.7 399.4 221.6 384.2 231 374.9L286 319.9L231 264.9C221.6 255.5 221.6 240.3 231 231z"/></svg>
|
||||
<span id="error-message"></span>
|
||||
</p>
|
||||
|
||||
<p id="success" class="validation-result">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M320 576C178.6 576 64 461.4 64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576zM438 209.7C427.3 201.9 412.3 204.3 404.5 215L285.1 379.2L233 327.1C223.6 317.7 208.4 317.7 199.1 327.1C189.8 336.5 189.7 351.7 199.1 361L271.1 433C276.1 438 282.9 440.5 289.9 440C296.9 439.5 303.3 435.9 307.4 430.2L443.3 243.2C451.1 232.5 448.7 217.5 438 209.7z"/></svg>
|
||||
<span>Validation Success</span>
|
||||
</p>
|
||||
|
||||
<p id="result-placeholder" class="validation-result"></p>
|
||||
|
||||
<p>Insert SPF description</p>
|
||||
|
||||
<center>
|
||||
<h3>More tools:</h3>
|
||||
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
||||
</center>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user