Implement SPF creator tool
This commit is contained in:
parent
00c8a999e7
commit
5b8a3d9266
@ -5,9 +5,12 @@
|
||||
export class Field {
|
||||
displayName = null;
|
||||
description = null;
|
||||
categoryName = null;
|
||||
isHidden = false;
|
||||
|
||||
constructor(key) {
|
||||
this.key = key;
|
||||
this.id = "field-" + key;
|
||||
}
|
||||
|
||||
// Virtual methods
|
||||
@ -31,4 +34,14 @@ export class Field {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
category(category) {
|
||||
this.categoryName = category;
|
||||
return this;
|
||||
}
|
||||
|
||||
hidden() {
|
||||
this.isHidden = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,44 +14,73 @@ export class SpfRecord {
|
||||
.pos(0),
|
||||
|
||||
new DomainMechanism("include")
|
||||
.label("Include")
|
||||
.desc("Also apply the SPF records from these domains")
|
||||
.multiple()
|
||||
.pos(1),
|
||||
|
||||
new DomainMechanism("a")
|
||||
.label("Domains")
|
||||
.desc("Select the IP address from these domains")
|
||||
.value(ValueRequirement.OPTIONAL)
|
||||
.multiple()
|
||||
.pos(1),
|
||||
|
||||
new DomainMechanism("mx")
|
||||
.label("MX Records")
|
||||
.desc("Select the IP addresses from the MX records of these domains (or current domain if none specified)")
|
||||
.value(ValueRequirement.OPTIONAL)
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainMechanism("ptr")
|
||||
.hidden()
|
||||
.value(ValueRequirement.OPTIONAL)
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new IPv4Mechanism("ipv4")
|
||||
.label("IPv4 addresses")
|
||||
.desc("Select these IP addresses")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new IPv6Mechanism("ipv6")
|
||||
.label("IPv6 addresses")
|
||||
.desc("Select these IP addresses")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new DomainMechanism("exists")
|
||||
.label("Exists")
|
||||
.desc("Apply only if this domain exists (can be used with macro expansions)")
|
||||
.category("advanced")
|
||||
.multiple()
|
||||
.pos(2),
|
||||
|
||||
new Mechanism("all")
|
||||
.label("All others")
|
||||
.desc("How to treat the rest of the IP addresses")
|
||||
.value(ValueRequirement.PROHIBITED)
|
||||
.pos(3),
|
||||
|
||||
new Modifier("redirect")
|
||||
.label("Redirect")
|
||||
.desc("Redirect to the SPF record of this domain if no IP addresses matched")
|
||||
.category("advanced")
|
||||
.pos(4),
|
||||
|
||||
new Modifier("exp")
|
||||
.label("Explanation")
|
||||
.desc("Points to a domain whose TXT record contains an error message if validation fails. Macros can be used here")
|
||||
.category("advanced")
|
||||
.pos(4),
|
||||
];
|
||||
|
||||
static categories = {
|
||||
"advanced": "Advanced",
|
||||
};
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
@ -121,4 +150,17 @@ export class SpfRecord {
|
||||
lastPos = term.position;
|
||||
}
|
||||
}
|
||||
|
||||
static fieldsToString() {
|
||||
const tokens = this.fields
|
||||
.filter(field => !field.isHidden)
|
||||
.filter(field => field.valueRequirement === ValueRequirement.REQUIRED ? field.getInputValue() : field.getInputQualifier())
|
||||
.map(field =>
|
||||
field.getInputQualifier()
|
||||
+ field.key
|
||||
+ (field.getInputValue() ? field.separator + field.getInputValue() : "")
|
||||
);
|
||||
|
||||
return tokens.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,11 +62,13 @@ export class TagListRecord {
|
||||
return true;
|
||||
}
|
||||
|
||||
fieldsToString() {
|
||||
let tokens = this.constructor.fields
|
||||
static fieldsToString() {
|
||||
const tokens = this.fields
|
||||
.filter(field => !field.isHidden)
|
||||
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
|
||||
.filter(field => field.getInputValue())
|
||||
.map(field => field.key + "=" + field.getInputValue());
|
||||
|
||||
return tokens.join("; ");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
import { Mechanism } from "./Mechanism.js";
|
||||
import { validateSpfDomain } from "./utils.js";
|
||||
|
||||
export class DomainMechanism extends Mechanism {
|
||||
separator = ":";
|
||||
placeholder = "example.com";
|
||||
|
||||
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`);
|
||||
if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { ValidationError } from "../ValidationError.js";
|
||||
import { Mechanism } from "./Mechanism.js";
|
||||
|
||||
export class IPv4Mechanism extends Mechanism {
|
||||
separator = ":";
|
||||
placeholder = "0.0.0.0";
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Mechanism } from "./Mechanism.js";
|
||||
|
||||
export class IPv6Mechanism extends Mechanism {
|
||||
placeholder = "2001:db8::1";
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
@ -1,9 +1,36 @@
|
||||
import { Term } from "./Term.js";
|
||||
import { ValueRequirement } from "./ValueRequirement.js";
|
||||
|
||||
export class Mechanism extends Term {
|
||||
separator = ":";
|
||||
placeholder = null;
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
getInputQualifier() {
|
||||
return document.getElementById(this.id + "-qualifier").value;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return document.getElementById(this.id + "-value")?.value;
|
||||
}
|
||||
|
||||
getInputHtml() {
|
||||
const noValue = this.valueRequirement === ValueRequirement.PROHIBITED;
|
||||
const placeholder = this.placeholder
|
||||
+ (this.valueRequirement === ValueRequirement.OPTIONAL ? " (Optional)" : "");
|
||||
|
||||
return `
|
||||
<select id="${this.id}-qualifier">
|
||||
${this.isRequired ? "" : `<option value=""><not set></option>`}
|
||||
<option value="+">Pass</option>
|
||||
<option value="-">Fail</option>
|
||||
<option value="~">Soft fail</option>
|
||||
<option value="?">Neutral</option>
|
||||
</select>
|
||||
${noValue ? "" : `<input id="${this.id}-value" type="text" placeholder="${placeholder}">`}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Term } from "./Term.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
import { validateSpfDomain } from "./utils.js";
|
||||
|
||||
export class Modifier extends Term {
|
||||
separator = "=";
|
||||
@ -8,8 +9,17 @@ export class Modifier extends Term {
|
||||
super(key);
|
||||
}
|
||||
|
||||
validate() {
|
||||
// TODO validate
|
||||
validate(value) {
|
||||
if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return document.getElementById(this.id).value;
|
||||
}
|
||||
|
||||
getInputHtml() {
|
||||
return `<input id="${this.id}" type="text" placeholder="example.com">`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,12 @@ export class Term extends Field {
|
||||
super(key)
|
||||
}
|
||||
|
||||
// Virtual methods
|
||||
|
||||
getInputQualifier() {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
|
||||
required() {
|
||||
|
||||
@ -14,4 +14,8 @@ export class VersionTerm extends Term {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
6
assets/scripts/spf/utils.js
Normal file
6
assets/scripts/spf/utils.js
Normal file
@ -0,0 +1,6 @@
|
||||
// https://www.rfc-editor.org/rfc/rfc7208#section-7.1
|
||||
export function validateSpfDomain(domain) {
|
||||
return domain
|
||||
.split(".")
|
||||
.every(segment => segment.match(/^([^%]|%_|%%|%-|%\{[slodiphcrtv]\d*r?[-.+,\/_=]*})+$/));
|
||||
}
|
||||
@ -4,12 +4,10 @@ import { Field } from "../Field.js";
|
||||
export class Tag extends Field {
|
||||
isRequired = false;
|
||||
defaultValue = null;
|
||||
categoryName = null;
|
||||
position = null;
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
this.id = "tag-" + key;
|
||||
}
|
||||
|
||||
// Virtual methods
|
||||
@ -30,11 +28,6 @@ export class Tag extends Field {
|
||||
return this;
|
||||
}
|
||||
|
||||
category(category) {
|
||||
this.categoryName = category;
|
||||
return this;
|
||||
}
|
||||
|
||||
pos(i) {
|
||||
this.position = i;
|
||||
return this;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { DmarcRecord } from "./records/DmarcRecord.js";
|
||||
import { DmarcRecord } from "../records/DmarcRecord.js";
|
||||
import { SpfRecord } from "../records/SpfRecord.js";
|
||||
|
||||
const records = {
|
||||
"/dmarc-creator": DmarcRecord,
|
||||
"/spf-creator": SpfRecord,
|
||||
};
|
||||
|
||||
const Record = records[location.pathname];
|
||||
@ -21,7 +23,7 @@ for (const category of categories) {
|
||||
|
||||
function addFields(elem, fields) {
|
||||
for (const field of fields) {
|
||||
if (!field.getInputHtml()) continue;
|
||||
if (field.isHidden || !field.getInputHtml()) continue;
|
||||
|
||||
elem.innerHTML += `
|
||||
<label for="${field.key}">${field.displayName}</label>
|
||||
@ -31,14 +33,11 @@ function addFields(elem, fields) {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("form").onchange = () => generate();
|
||||
|
||||
document.getElementById("form").onchange = generate;
|
||||
generate();
|
||||
|
||||
function generate() {
|
||||
const record = new Record();
|
||||
|
||||
document.getElementById("record").value = record.fieldsToString();
|
||||
function generate(event) {
|
||||
document.getElementById("record").value = Record.fieldsToString();
|
||||
}
|
||||
|
||||
document.getElementById("record").onclick = (e) => {
|
||||
@ -1,5 +1,5 @@
|
||||
import { DmarcRecord } from "./records/DmarcRecord.js";
|
||||
import { SpfRecord } from "./records/SpfRecord.js";
|
||||
import { DmarcRecord } from "../records/DmarcRecord.js";
|
||||
import { SpfRecord } from "../records/SpfRecord.js";
|
||||
|
||||
const records = {
|
||||
"/dmarc-validator": DmarcRecord,
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>DMARC Record Creator - Generate DMARC DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/creator.js"></script>
|
||||
<script type="module" src="/assets/scripts/ui/creator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DMARC Record Creator</h1>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>DMARC Record Validator - Validate DMARC DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/validator.js"></script>
|
||||
<script type="module" src="/assets/scripts/ui/validator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DMARC Record Validator</h1>
|
||||
|
||||
59
spf-creator/index.html
Normal file
59
spf-creator/index.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SPF Record Creator - Generate SPF DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/ui/creator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SPF Record Creator</h1>
|
||||
|
||||
<label for="record">DNS Record</label><br>
|
||||
<input id="record" type="text" readonly>
|
||||
|
||||
<main>
|
||||
<h2>Create an SPF DNS Record</h2>
|
||||
|
||||
<p>
|
||||
Customize the options below to generate an SPF record, which will be shown in the input field above.
|
||||
The qualifiers that can be used are:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>Pass</b> (Default): Selected IP addresses are authorized to send emails from this domain<br>
|
||||
<b>Fail</b>: Selected IP addresses are NOT authorized to send emails from this domain<br>
|
||||
<b>Soft fail</b>: Selected IP addresses might not be authorized to send emails from this domain (actual behavior differs)<br>
|
||||
<b>Neutral</b>: Do not explicitly state whether the IP addresses are authorized or not (can be used for overriding other qualifiers)
|
||||
</p>
|
||||
|
||||
<form id="form"></form>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
This tool allows you to create SPF DNS records, which are used to authorize emails, and are necessary
|
||||
to be able to send emails to most providers, such as Gmail, Outlook and Yahoo Mail.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It works by specifying which IP addresses are authorized or not authorized to send emails.
|
||||
You can specify IP addresses from a domain, from your MX records, or explicitly.
|
||||
When specifying IP addresses, you then say whether these should pass or fail the validation.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For advanced usage, domain fields may contain macros. These start with a percentage sign and will expand
|
||||
to a dynamic value. For example, <b>%{d}</b> expands to the current domain and <b>%{i}</b> to the
|
||||
current IP address. See
|
||||
<a href="https://www.rfc-editor.org/rfc/rfc7208#section-7.2" target="_blank">the SPF specification</a>
|
||||
for a list of macros you can use.
|
||||
</p>
|
||||
|
||||
<center>
|
||||
<h3>More tools:</h3>
|
||||
<a href="/spf-validator">SPF Validator Tool</a>
|
||||
</center>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script type="module" src="/assets/scripts/ui/validator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SPF Record Validator</h1>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user