diff --git a/assets/scripts/creator.js b/assets/scripts/creator.js index 97150ff..ae749fb 100644 --- a/assets/scripts/creator.js +++ b/assets/scripts/creator.js @@ -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; } diff --git a/assets/scripts/fields/ConstantField.js b/assets/scripts/fields/ConstantField.js index 50bd693..5ef2fcb 100644 --- a/assets/scripts/fields/ConstantField.js +++ b/assets/scripts/fields/ConstantField.js @@ -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; diff --git a/assets/scripts/fields/DmarcUriListField.js b/assets/scripts/fields/DmarcUriListField.js index b90552d..0f1f412 100644 --- a/assets/scripts/fields/DmarcUriListField.js +++ b/assets/scripts/fields/DmarcUriListField.js @@ -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); diff --git a/assets/scripts/fields/DomainField.js b/assets/scripts/fields/DomainField.js new file mode 100644 index 0000000..634a152 --- /dev/null +++ b/assets/scripts/fields/DomainField.js @@ -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; + } +} diff --git a/assets/scripts/fields/EnumField.js b/assets/scripts/fields/EnumField.js index 55e64ef..8a5f2cc 100644 --- a/assets/scripts/fields/EnumField.js +++ b/assets/scripts/fields/EnumField.js @@ -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; diff --git a/assets/scripts/fields/Field.js b/assets/scripts/fields/Field.js index b54614f..abdd651 100644 --- a/assets/scripts/fields/Field.js +++ b/assets/scripts/fields/Field.js @@ -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; + pos(i) { + this.position = i; return this; } - - after(field) { - this.afterFieldName = field; - return this; - } -} \ No newline at end of file +} diff --git a/assets/scripts/fields/IntField.js b/assets/scripts/fields/IntField.js index dddaa68..4d29b2c 100644 --- a/assets/scripts/fields/IntField.js +++ b/assets/scripts/fields/IntField.js @@ -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; diff --git a/assets/scripts/tools/DmarcTool.js b/assets/scripts/tools/DmarcTool.js index c8cbd71..3037c49 100644 --- a/assets/scripts/tools/DmarcTool.js +++ b/assets/scripts/tools/DmarcTool.js @@ -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) diff --git a/assets/scripts/tools/DnsTool.js b/assets/scripts/tools/DnsTool.js index f198c24..16e4b6a 100644 --- a/assets/scripts/tools/DnsTool.js +++ b/assets/scripts/tools/DnsTool.js @@ -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; diff --git a/assets/scripts/tools/SpfTool.js b/assets/scripts/tools/SpfTool.js new file mode 100644 index 0000000..ab0c396 --- /dev/null +++ b/assets/scripts/tools/SpfTool.js @@ -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+/); + } +} diff --git a/assets/scripts/validator.js b/assets/scripts/validator.js index 0a1e7e6..7ce3b1e 100644 --- a/assets/scripts/validator.js +++ b/assets/scripts/validator.js @@ -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]; diff --git a/assets/styles/main.css b/assets/styles/main.css index 8b7f7c8..a881e6c 100644 --- a/assets/styles/main.css +++ b/assets/styles/main.css @@ -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); } diff --git a/dmarc-creator/index.html b/dmarc-creator/index.html index 73d533d..4d87a9b 100644 --- a/dmarc-creator/index.html +++ b/dmarc-creator/index.html @@ -10,7 +10,7 @@

DMARC Record Creator


- +

Create a DMARC DNS Record

diff --git a/spf-validator/index.html b/spf-validator/index.html new file mode 100644 index 0000000..80829c2 --- /dev/null +++ b/spf-validator/index.html @@ -0,0 +1,38 @@ + + + + + SPF Record Validator - Validate SPF DNS Records + + + + +

SPF Record Validator

+ +
+ + +
+

Paste an SPF DNS record into the input field to check if it is valid.

+ +

+ + +

+ +

+ + Validation Success +

+ +

+ +

Insert SPF description

+ +
+

More tools:

+ DMARC Validator Tool +
+
+ +