Skip to content

Commit 53f5cf9

Browse files
committed
Initial code for mta-sts
Signed-off-by: Gunni <github.march@s.meh.is>
1 parent 30d1642 commit 53f5cf9

File tree

7 files changed

+369
-0
lines changed

7 files changed

+369
-0
lines changed

mta-sts-template/.gitignore

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Logs
2+
3+
logs
4+
_.log
5+
npm-debug.log_
6+
yarn-debug.log*
7+
yarn-error.log*
8+
lerna-debug.log*
9+
.pnpm-debug.log*
10+
11+
# Diagnostic reports (https://nodejs.org/api/report.html)
12+
13+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14+
15+
# Runtime data
16+
17+
pids
18+
_.pid
19+
_.seed
20+
\*.pid.lock
21+
22+
# Directory for instrumented libs generated by jscoverage/JSCover
23+
24+
lib-cov
25+
26+
# Coverage directory used by tools like istanbul
27+
28+
coverage
29+
\*.lcov
30+
31+
# nyc test coverage
32+
33+
.nyc_output
34+
35+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36+
37+
.grunt
38+
39+
# Bower dependency directory (https://bower.io/)
40+
41+
bower_components
42+
43+
# node-waf configuration
44+
45+
.lock-wscript
46+
47+
# Compiled binary addons (https://nodejs.org/api/addons.html)
48+
49+
build/Release
50+
51+
# Dependency directories
52+
53+
node_modules/
54+
jspm_packages/
55+
56+
# Snowpack dependency directory (https://snowpack.dev/)
57+
58+
web_modules/
59+
60+
# TypeScript cache
61+
62+
\*.tsbuildinfo
63+
64+
# Optional npm cache directory
65+
66+
.npm
67+
68+
# Optional eslint cache
69+
70+
.eslintcache
71+
72+
# Optional stylelint cache
73+
74+
.stylelintcache
75+
76+
# Microbundle cache
77+
78+
.rpt2_cache/
79+
.rts2_cache_cjs/
80+
.rts2_cache_es/
81+
.rts2_cache_umd/
82+
83+
# Optional REPL history
84+
85+
.node_repl_history
86+
87+
# Output of 'npm pack'
88+
89+
\*.tgz
90+
91+
# Yarn Integrity file
92+
93+
.yarn-integrity
94+
95+
# parcel-bundler cache (https://parceljs.org/)
96+
97+
.cache
98+
.parcel-cache
99+
100+
# Next.js build output
101+
102+
.next
103+
out
104+
105+
# Nuxt.js build / generate output
106+
107+
.nuxt
108+
dist
109+
110+
# Gatsby files
111+
112+
.cache/
113+
114+
# Comment in the public line in if your project uses Gatsby and not Next.js
115+
116+
# https://nextjs.org/blog/next-9-1#public-directory-support
117+
118+
# public
119+
120+
# vuepress build output
121+
122+
.vuepress/dist
123+
124+
# vuepress v2.x temp and cache directory
125+
126+
.temp
127+
.cache
128+
129+
# Docusaurus cache and generated files
130+
131+
.docusaurus
132+
133+
# Serverless directories
134+
135+
.serverless/
136+
137+
# FuseBox cache
138+
139+
.fusebox/
140+
141+
# DynamoDB Local files
142+
143+
.dynamodb/
144+
145+
# TernJS port file
146+
147+
.tern-port
148+
149+
# Stores VSCode versions used for testing VSCode extensions
150+
151+
.vscode-test
152+
153+
# yarn v2
154+
155+
.yarn/cache
156+
.yarn/unplugged
157+
.yarn/build-state.yml
158+
.yarn/install-state.gz
159+
.pnp.\*
160+
161+
# wrangler project
162+
163+
.dev.vars*
164+
!.dev.vars.example
165+
.env*
166+
!.env.example
167+
.wrangler/

mta-sts-template/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# MTA-STS Cloudflare worker
2+
3+
[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/mta-sts-template)
4+
5+
<!-- dash-content-start -->
6+
7+
This is a [mta-sts](todo) worker, it satisfies the web part of RFC 8461, note that the DNS part is your responsibility.
8+
9+
<!-- dash-content-end -->
10+
11+
When deploying this worker you add it as a custom sub-domain named `mta-sts` under the domain you want to enable MTA-STS for.
12+
13+
After ensuring it works and is publishing the correct content you need to add a TXT DNS record at `_mta-sts` with a value of this format `"v=STSv1; id=20260220T211000;"`.

mta-sts-template/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mta-sts-template/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "mta-sts",
3+
"private": true,
4+
"scripts": {
5+
"deploy": "wrangler deploy",
6+
"dev": "wrangler dev",
7+
"start": "wrangler dev"
8+
},
9+
"env": {
10+
"MAX_AGE": 60,
11+
"MODE": "testing",
12+
"MX": "mx1.example.com\nmx2.example.com"
13+
},
14+
"devDependencies": {
15+
"wrangler": "^4.37.1"
16+
},
17+
"cloudflare": {
18+
"label": "MTA-STS",
19+
"products": [
20+
"Workers"
21+
],
22+
"categories": [
23+
"static"
24+
],
25+
"docs_url": "https://developers.cloudflare.com/workers/",
26+
"icon_urls": [],
27+
"preview_image_url": "https://fast.image.delivery/nnoriwy.png",
28+
"publish": true
29+
}
30+
}

mta-sts-template/src/index.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export default {
2+
async fetch(request, env, ctx) {
3+
const url = new URL(request.url);
4+
const policyPath = '/.well-known/mta-sts.txt';
5+
6+
// recommended MAX_AGEs:
7+
// 1 week: 604800 (good for testing)
8+
// 1 month: 2628000 (good for prod?)
9+
// 1 year: 31557600 (you cannot go higher as defined in RFC 8461)
10+
11+
// max_age: non-negative integer, max 31,557,600 (RFC 8461 §3.2)
12+
const MAX_AGE_MIN = 0; // if MAX_AGE < MAX_AGE_MIN then we use MAX_AGE_DEFAULT
13+
const MAX_AGE_DEFAULT = 60; // only used until you set environment variable MAX_AGE
14+
const MAX_AGE_MAX = 31_557_600; // if MAX_AGE > MAX_AGE_MAX then we use MAX_AGE_MAX
15+
16+
const rawMaxAge = String(env.MAX_AGE ?? '').trim();
17+
const parsedMaxAge = rawMaxAge === '' ? NaN : parseInt(rawMaxAge, 10);
18+
const maxAge = !Number.isInteger(parsedMaxAge) || parsedMaxAge < 0
19+
? MAX_AGE_DEFAULT // RFC: Non-negative integer required
20+
: parsedMaxAge < MAX_AGE_MIN
21+
? MAX_AGE_DEFAULT // Your choice: reset to default if too low
22+
: parsedMaxAge > MAX_AGE_MAX
23+
? MAX_AGE_MAX // RFC: Usually 1 year (31557600)
24+
: parsedMaxAge;
25+
26+
const defaultHeaders = {
27+
'Allow': 'GET, HEAD',
28+
'Content-Type': 'text/plain; charset=utf-8',
29+
// Optional
30+
'Strict-Transport-Security': `max-age=${maxAge}`,
31+
};
32+
33+
// Only GET/HEAD are allowed
34+
const method = request.method.toUpperCase();
35+
if (method !== 'GET' && method !== 'HEAD') {
36+
const h = new Headers({ ...defaultHeaders, 'Allow': 'GET, HEAD' });
37+
return new Response('Method Not Allowed', { status: 405, headers: h });
38+
}
39+
40+
// Redirect everything that's not the canonical path to the policy URL
41+
// Dedicated hostname => cache redirect aggressively
42+
if (url.pathname !== policyPath) {
43+
const target = new URL(policyPath, url.origin).toString();
44+
const redirectHeaders = new Headers({
45+
...defaultHeaders,
46+
'Location': target,
47+
'Cache-Control': `public, max-age=${maxAge}, immutable`,
48+
});
49+
return new Response(null, { status: 308, headers: redirectHeaders });
50+
}
51+
52+
// mode: enforce | testing | none
53+
const allowedModes = new Set(['enforce', 'testing', 'none']);
54+
const modeRaw = (env.MODE || 'none').toLowerCase();
55+
const mode = allowedModes.has(modeRaw) ? modeRaw : 'none';
56+
57+
// mx: only applicable for enforce/testing
58+
const mxLines =
59+
mode !== 'none'
60+
? String(env.MX_HOSTS || '')
61+
.split(/[,\s]+/)
62+
.map((s) => s.trim())
63+
.filter(Boolean)
64+
.map((mx) => `mx: ${mx}`)
65+
: [];
66+
67+
// Enforce RFC rule: mx required when mode is enforce/testing
68+
if ((mode === 'enforce' || mode === 'testing') && mxLines.length === 0) {
69+
const h = new Headers({ ...defaultHeaders });
70+
return new Response(
71+
'Misconfigured Worker: MX_HOSTS required when MODE is "enforce" or "testing".',
72+
{ status: 500, headers: h }
73+
);
74+
}
75+
76+
// For mode "none", omit mx lines in the policy output
77+
const lines = [
78+
'version: STSv1',
79+
`mode: ${mode}`,
80+
...mxLines,
81+
`max_age: ${maxAge}`,
82+
];
83+
const body = lines.join('\n') + '\n'; // add newline at end of file
84+
const headers = new Headers(defaultHeaders);
85+
86+
if (method == 'HEAD') {
87+
return new Response(null, { status: 200, headers });
88+
}
89+
90+
return new Response(body, { status: 200, headers });
91+
},
92+
};

mta-sts-template/tsconfig.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
4+
5+
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
6+
"allowJs": true,
7+
/* Enable error reporting in type-checked JavaScript files. */
8+
"checkJs": false,
9+
10+
/* Disable emitting files from a compilation. */
11+
"noEmit": true,
12+
13+
/* Ensure that each file can be safely transpiled without relying on other imports. */
14+
"isolatedModules": true,
15+
16+
/* Ensure that casing is correct in imports. */
17+
"forceConsistentCasingInFileNames": true,
18+
19+
/* Enable all strict type-checking options. */
20+
"strict": true,
21+
22+
"lib": ["es2015", "dom"]
23+
}
24+
}

mta-sts-template/wrangler.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name = "mta-sts"
2+
main = "src/index.js"
3+
compatibility_date = "2026-02-21"
4+
5+
[env.test_none.vars]
6+
# RFC 8461: max_age must be non-negative.
7+
# 60 = 1 minute (Good starting point when starting)
8+
MODE = "none"
9+
MAX_AGE = "60"
10+
11+
[env.test_testing.vars]
12+
# RFC 8461: max_age must be non-negative.
13+
# 60 = 1 minute (Good starting point when starting)
14+
MODE = "testing"
15+
MAX_AGE = "bla"
16+
MX_HOSTS = "mx1.example.com mx2.example.com"
17+
18+
[env.enforce_proton.vars]
19+
# RFC 8461: max_age must be non-negative.
20+
# 604800 = 1 week (Good starting point)
21+
MAX_AGE = 60
22+
MODE = "enforce"
23+
MX_HOSTS = "mail.protonmail.ch mailsec.protonmail.ch"
24+
25+
[env.enforce_protonalias.vars]
26+
# RFC 8461: max_age must be non-negative.
27+
# 604800 = 1 week (Good starting point)
28+
MAX_AGE = 60
29+
MODE = "enforce"
30+
MX_HOSTS = "mx1.alias.proton.me mx2.alias.proton.me"

0 commit comments

Comments
 (0)