Implement SPF creator tool
This commit is contained in:
parent
00c8a999e7
commit
5b8a3d9266
@ -5,9 +5,12 @@
|
|||||||
export class Field {
|
export class Field {
|
||||||
displayName = null;
|
displayName = null;
|
||||||
description = null;
|
description = null;
|
||||||
|
categoryName = null;
|
||||||
|
isHidden = false;
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
this.id = "field-" + key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
@ -31,4 +34,14 @@ export class Field {
|
|||||||
this.description = description;
|
this.description = description;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
category(category) {
|
||||||
|
this.categoryName = category;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden() {
|
||||||
|
this.isHidden = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,44 +14,73 @@ export class SpfRecord {
|
|||||||
.pos(0),
|
.pos(0),
|
||||||
|
|
||||||
new DomainMechanism("include")
|
new DomainMechanism("include")
|
||||||
|
.label("Include")
|
||||||
|
.desc("Also apply the SPF records from these domains")
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(1),
|
.pos(1),
|
||||||
|
|
||||||
new DomainMechanism("a")
|
new DomainMechanism("a")
|
||||||
|
.label("Domains")
|
||||||
|
.desc("Select the IP address from these domains")
|
||||||
|
.value(ValueRequirement.OPTIONAL)
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(1),
|
.pos(1),
|
||||||
|
|
||||||
new DomainMechanism("mx")
|
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()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new DomainMechanism("ptr")
|
new DomainMechanism("ptr")
|
||||||
|
.hidden()
|
||||||
|
.value(ValueRequirement.OPTIONAL)
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new IPv4Mechanism("ipv4")
|
new IPv4Mechanism("ipv4")
|
||||||
|
.label("IPv4 addresses")
|
||||||
|
.desc("Select these IP addresses")
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new IPv6Mechanism("ipv6")
|
new IPv6Mechanism("ipv6")
|
||||||
|
.label("IPv6 addresses")
|
||||||
|
.desc("Select these IP addresses")
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new DomainMechanism("exists")
|
new DomainMechanism("exists")
|
||||||
|
.label("Exists")
|
||||||
|
.desc("Apply only if this domain exists (can be used with macro expansions)")
|
||||||
|
.category("advanced")
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new Mechanism("all")
|
new Mechanism("all")
|
||||||
|
.label("All others")
|
||||||
|
.desc("How to treat the rest of the IP addresses")
|
||||||
.value(ValueRequirement.PROHIBITED)
|
.value(ValueRequirement.PROHIBITED)
|
||||||
.pos(3),
|
.pos(3),
|
||||||
|
|
||||||
new Modifier("redirect")
|
new Modifier("redirect")
|
||||||
|
.label("Redirect")
|
||||||
|
.desc("Redirect to the SPF record of this domain if no IP addresses matched")
|
||||||
|
.category("advanced")
|
||||||
.pos(4),
|
.pos(4),
|
||||||
|
|
||||||
new Modifier("exp")
|
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),
|
.pos(4),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static categories = {
|
||||||
|
"advanced": "Advanced",
|
||||||
|
};
|
||||||
|
|
||||||
constructor(text) {
|
constructor(text) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
}
|
}
|
||||||
@ -121,4 +150,17 @@ export class SpfRecord {
|
|||||||
lastPos = term.position;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldsToString() {
|
static fieldsToString() {
|
||||||
let tokens = this.constructor.fields
|
const tokens = this.fields
|
||||||
|
.filter(field => !field.isHidden)
|
||||||
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
|
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
|
||||||
.filter(field => field.getInputValue())
|
.filter(field => field.getInputValue())
|
||||||
.map(field => field.key + "=" + field.getInputValue());
|
.map(field => field.key + "=" + field.getInputValue());
|
||||||
|
|
||||||
return tokens.join("; ");
|
return tokens.join("; ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
import { Mechanism } from "./Mechanism.js";
|
import { Mechanism } from "./Mechanism.js";
|
||||||
|
import { validateSpfDomain } from "./utils.js";
|
||||||
|
|
||||||
export class DomainMechanism extends Mechanism {
|
export class DomainMechanism extends Mechanism {
|
||||||
separator = ":";
|
placeholder = "example.com";
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(value) {
|
validate(value) {
|
||||||
// https://www.rfc-editor.org/rfc/rfc7208#section-7.1
|
if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { ValidationError } from "../ValidationError.js";
|
|||||||
import { Mechanism } from "./Mechanism.js";
|
import { Mechanism } from "./Mechanism.js";
|
||||||
|
|
||||||
export class IPv4Mechanism extends Mechanism {
|
export class IPv4Mechanism extends Mechanism {
|
||||||
separator = ":";
|
placeholder = "0.0.0.0";
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(key);
|
super(key);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Mechanism } from "./Mechanism.js";
|
import { Mechanism } from "./Mechanism.js";
|
||||||
|
|
||||||
export class IPv6Mechanism extends Mechanism {
|
export class IPv6Mechanism extends Mechanism {
|
||||||
|
placeholder = "2001:db8::1";
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,36 @@
|
|||||||
import { Term } from "./Term.js";
|
import { Term } from "./Term.js";
|
||||||
|
import { ValueRequirement } from "./ValueRequirement.js";
|
||||||
|
|
||||||
export class Mechanism extends Term {
|
export class Mechanism extends Term {
|
||||||
separator = ":";
|
separator = ":";
|
||||||
|
placeholder = null;
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(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 { Term } from "./Term.js";
|
||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
import { validateSpfDomain } from "./utils.js";
|
||||||
|
|
||||||
export class Modifier extends Term {
|
export class Modifier extends Term {
|
||||||
separator = "=";
|
separator = "=";
|
||||||
@ -8,8 +9,17 @@ export class Modifier extends Term {
|
|||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate() {
|
validate(value) {
|
||||||
// TODO validate
|
if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
|
||||||
|
|
||||||
return true;
|
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)
|
super(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Virtual methods
|
||||||
|
|
||||||
|
getInputQualifier() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
// Builder methods
|
// Builder methods
|
||||||
|
|
||||||
required() {
|
required() {
|
||||||
|
|||||||
@ -14,4 +14,8 @@ export class VersionTerm extends Term {
|
|||||||
|
|
||||||
return true;
|
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 {
|
export class Tag extends Field {
|
||||||
isRequired = false;
|
isRequired = false;
|
||||||
defaultValue = null;
|
defaultValue = null;
|
||||||
categoryName = null;
|
|
||||||
position = null;
|
position = null;
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(key);
|
super(key);
|
||||||
this.id = "tag-" + key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
@ -30,11 +28,6 @@ export class Tag extends Field {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
category(category) {
|
|
||||||
this.categoryName = category;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos(i) {
|
pos(i) {
|
||||||
this.position = i;
|
this.position = i;
|
||||||
return this;
|
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 = {
|
const records = {
|
||||||
"/dmarc-creator": DmarcRecord,
|
"/dmarc-creator": DmarcRecord,
|
||||||
|
"/spf-creator": SpfRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Record = records[location.pathname];
|
const Record = records[location.pathname];
|
||||||
@ -21,7 +23,7 @@ for (const category of categories) {
|
|||||||
|
|
||||||
function addFields(elem, fields) {
|
function addFields(elem, fields) {
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (!field.getInputHtml()) continue;
|
if (field.isHidden || !field.getInputHtml()) continue;
|
||||||
|
|
||||||
elem.innerHTML += `
|
elem.innerHTML += `
|
||||||
<label for="${field.key}">${field.displayName}</label>
|
<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();
|
generate();
|
||||||
|
|
||||||
function generate() {
|
function generate(event) {
|
||||||
const record = new Record();
|
document.getElementById("record").value = Record.fieldsToString();
|
||||||
|
|
||||||
document.getElementById("record").value = record.fieldsToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("record").onclick = (e) => {
|
document.getElementById("record").onclick = (e) => {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { DmarcRecord } from "./records/DmarcRecord.js";
|
import { DmarcRecord } from "../records/DmarcRecord.js";
|
||||||
import { SpfRecord } from "./records/SpfRecord.js";
|
import { SpfRecord } from "../records/SpfRecord.js";
|
||||||
|
|
||||||
const records = {
|
const records = {
|
||||||
"/dmarc-validator": DmarcRecord,
|
"/dmarc-validator": DmarcRecord,
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>DMARC Record Creator - Generate DMARC DNS Records</title>
|
<title>DMARC Record Creator - Generate DMARC DNS Records</title>
|
||||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>DMARC Record Creator</h1>
|
<h1>DMARC Record Creator</h1>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>DMARC Record Validator - Validate DMARC DNS Records</title>
|
<title>DMARC Record Validator - Validate DMARC DNS Records</title>
|
||||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>DMARC Record Validator</h1>
|
<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">
|
<meta charset="UTF-8">
|
||||||
<title>SPF Record Validator - Validate SPF DNS Records</title>
|
<title>SPF Record Validator - Validate SPF DNS Records</title>
|
||||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>SPF Record Validator</h1>
|
<h1>SPF Record Validator</h1>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user