asdf
This commit is contained in:
1 parent
2b7ac42d6b
commit
76cedf8400
7 files changed
+41
-60
No files matched your search
@@ -74,7 +74,7 @@ Environment; see *Contact form* below.
|
|||||||
and no third party is involved, so there is nothing to consent to. The form therefore needs
|
and no third party is involved, so there is nothing to consent to. The form therefore needs
|
||||||
JavaScript; without it a `<noscript>` line points to the email address.
|
JavaScript; without it a `<noscript>` line points to the email address.
|
||||||
|
|
||||||
In the container, nginx proxies `/contact`, `/contact/challenge`, `/notify` and `/notify/verify` to the handler on `127.0.0.1:3000`; both are started
|
In the container, nginx proxies `/contact`, `/contact/challenge`, `/notify` and `GET /notify/verify` to the handler on `127.0.0.1:3000`; both are started
|
||||||
by `docker/entrypoint.sh`, which restarts the handler if it crashes. If the handler is down,
|
by `docker/entrypoint.sh`, which restarts the handler if it crashes. If the handler is down,
|
||||||
visitors are sent to the error page. Without SMTP settings the handler does not start and the
|
visitors are sent to the error page. Without SMTP settings the handler does not start and the
|
||||||
rest of the site is served as usual.
|
rest of the site is served as usual.
|
||||||
@@ -98,11 +98,11 @@ Environment; see *Contact form* below.
|
|||||||
shows a coming-soon page whose form posts `email`, `consent=yes`, `lang`,
|
shows a coming-soon page whose form posts `email`, `consent=yes`, `lang`,
|
||||||
`list=cipherverifier-alpha`, the honeypot `website` and the proof-of-work answer `altcha` to
|
`list=cipherverifier-alpha`, the honeypot `website` and the proof-of-work answer `altcha` to
|
||||||
`POST /notify`, handled by `server/contact.mjs` like the contact form. It is double opt-in
|
`POST /notify`, handled by `server/contact.mjs` like the contact form. It is double opt-in
|
||||||
without a database: the handler emails the address a link to `/notify/confirm` carrying a
|
without a database: the handler emails the address a link to `GET /notify/verify` carrying a
|
||||||
signed token (address, language, time of consent; valid 7 days) and keeps nothing. That page
|
signed token (address, language, time of consent; valid 7 days) and keeps nothing. Opening the
|
||||||
posts the token to `POST /notify/verify` with a button (mail scanners open links, they do not
|
link confirms: the handler sends the confirmed registration, with the consent wording and both
|
||||||
press buttons), and the handler sends the confirmed registration, with the consent wording and
|
times, to `CONTACT_TO`, and redirects to `/notify/confirmed`. A mail scanner that opens every
|
||||||
both times, to `CONTACT_TO`. That mailbox is the list. The privacy page promises deletion
|
link in a message will confirm it too. Old `/notify/confirm` links are redirected by nginx. That mailbox is the list. The privacy page promises deletion
|
||||||
after the alpha mail is sent or on withdrawal. The result pages `/notify/confirmed` and
|
after the alpha mail is sent or on withdrawal. The result pages `/notify/confirmed` and
|
||||||
`/notify/error` are `noindex` and left out of the sitemap. Set the flag to false to bring back
|
`/notify/error` are `noindex` and left out of the sitemap. Set the flag to false to bring back
|
||||||
the full product page.
|
the full product page.
|
||||||
|
|||||||
+14
-2
@@ -51,8 +51,8 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_read_timeout 10s;
|
proxy_read_timeout 10s;
|
||||||
}
|
}
|
||||||
# The alpha sign-up, same handler: the form, and the confirmation from the link in its email.
|
# The alpha sign-up, same handler: the form posts here.
|
||||||
location ~ ^/notify(/verify)?$ {
|
location = /notify {
|
||||||
limit_except POST { deny all; }
|
limit_except POST { deny all; }
|
||||||
client_max_body_size 64k;
|
client_max_body_size 64k;
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
@@ -63,6 +63,18 @@ server {
|
|||||||
proxy_intercept_errors on;
|
proxy_intercept_errors on;
|
||||||
error_page 502 503 504 = @notify_down;
|
error_page 502 503 504 = @notify_down;
|
||||||
}
|
}
|
||||||
|
# The link in the confirmation email; opening it confirms.
|
||||||
|
location = /notify/verify {
|
||||||
|
limit_except GET { deny all; }
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
proxy_intercept_errors on;
|
||||||
|
error_page 502 503 504 = @notify_down;
|
||||||
|
}
|
||||||
|
# Links sent before the confirmation moved off the button page.
|
||||||
|
location ~ ^(/de)?/notify/confirm$ { return 302 /notify/verify$is_args$args; }
|
||||||
location @notify_down {
|
location @notify_down {
|
||||||
if ($http_referer ~ "/de(/|$|#)") { return 303 /de/notify/error; }
|
if ($http_referer ~ "/de(/|$|#)") { return 303 /de/notify/error; }
|
||||||
return 303 /notify/error;
|
return 303 /notify/error;
|
||||||
|
|||||||
+15
-12
@@ -9,12 +9,11 @@
|
|||||||
* A post without a solved challenge is refused, so the form needs scripting; the address next
|
* A post without a solved challenge is refused, so the form needs scripting; the address next
|
||||||
* to it does not.
|
* to it does not.
|
||||||
*
|
*
|
||||||
* Alpha sign-up (POST /notify, POST /notify/verify). Double opt-in without a database: the form
|
* Alpha sign-up (POST /notify, GET /notify/verify). Double opt-in without a database: the form
|
||||||
* posts email, consent=yes, list, lang, website and altcha; the handler emails the address a link
|
* posts email, consent=yes, list, lang, website and altcha; the handler emails the address a link
|
||||||
* to /notify/confirm carrying a signed token (address, language, time of consent, expiry). That
|
* to /notify/verify carrying a signed token (address, language, time of consent, expiry). Opening
|
||||||
* page posts the token back to /notify/verify, which checks the signature and sends the confirmed
|
* the link confirms: the handler checks the signature, sends the confirmed registration to
|
||||||
* registration to CONTACT_TO. The office mailbox is the list. The confirmation is a button, not
|
* CONTACT_TO and redirects to /notify/confirmed. The office mailbox is the list.
|
||||||
* the link itself, because mail scanners open every link in a message.
|
|
||||||
*
|
*
|
||||||
* Configuration comes from the environment only, see README.md.
|
* Configuration comes from the environment only, see README.md.
|
||||||
*/
|
*/
|
||||||
@@ -268,7 +267,7 @@ async function notify(req, res, form, lang) {
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const token = sign({ email, lang, consented: now, expires: now + CONFIRM_TTL_MS });
|
const token = sign({ email, lang, consented: now, expires: now + CONFIRM_TTL_MS });
|
||||||
const link = `${SITE}${lang === 'de' ? '/de' : ''}/notify/confirm?t=${token}`;
|
const link = `${SITE}/notify/verify?t=${token}`;
|
||||||
try {
|
try {
|
||||||
await transport.sendMail({
|
await transport.sendMail({
|
||||||
from: { name: 'CipherCell', address: FROM },
|
from: { name: 'CipherCell', address: FROM },
|
||||||
@@ -282,10 +281,10 @@ async function notify(req, res, form, lang) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Step two: the link's token comes back; the confirmed registration goes to the office. */
|
/** Step two: the link is opened; the confirmed registration goes to the office. */
|
||||||
async function verify(req, res, form) {
|
async function verify(req, res, query) {
|
||||||
const t = unsign(form.get('t'));
|
const t = unsign(query.get('t'));
|
||||||
const lang = t?.lang === 'de' ? 'de' : form.get('lang') === 'de' ? 'de' : 'en';
|
const lang = t?.lang === 'de' ? 'de' : 'en';
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!t || typeof t.email !== 'string' || !(t.expires > now) || !(t.consented > 0)) {
|
if (!t || typeof t.email !== 'string' || !(t.expires > now) || !(t.consented > 0)) {
|
||||||
redirect(res, lang, '/notify/error');
|
redirect(res, lang, '/notify/error');
|
||||||
@@ -324,16 +323,20 @@ async function verify(req, res, form) {
|
|||||||
// nginx sets X-Real-IP from the proxy chain; fall back to the socket for local runs.
|
// nginx sets X-Real-IP from the proxy chain; fall back to the socket for local runs.
|
||||||
const clientIp = (req) => String(req.headers['x-real-ip'] || req.socket.remoteAddress || 'unknown');
|
const clientIp = (req) => String(req.headers['x-real-ip'] || req.socket.remoteAddress || 'unknown');
|
||||||
|
|
||||||
const ROUTES = { '/contact': contact, '/notify': notify, '/notify/verify': verify };
|
const ROUTES = { '/contact': contact, '/notify': notify };
|
||||||
|
|
||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
const path = (req.url || '').split('?')[0];
|
const [path, search = ''] = (req.url || '').split('?');
|
||||||
if (path === '/contact/healthz') { res.writeHead(200).end('ok\n'); return; }
|
if (path === '/contact/healthz') { res.writeHead(200).end('ok\n'); return; }
|
||||||
if (path === '/contact/challenge' && req.method === 'GET') {
|
if (path === '/contact/challenge' && req.method === 'GET') {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
||||||
res.end(JSON.stringify(await issueChallenge()));
|
res.end(JSON.stringify(await issueChallenge()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (path === '/notify/verify' && req.method === 'GET') {
|
||||||
|
await verify(req, res, new URLSearchParams(search));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const route = ROUTES[path];
|
const route = ROUTES[path];
|
||||||
if (req.method !== 'POST' || !route) { res.writeHead(404).end(); return; }
|
if (req.method !== 'POST' || !route) { res.writeHead(404).end(); return; }
|
||||||
|
|
||||||
|
|||||||
+4
-14
@@ -54,8 +54,8 @@ const en = {
|
|||||||
},
|
},
|
||||||
manifesto: {
|
manifesto: {
|
||||||
lines: ['Security built in,', 'not bolted on.'],
|
lines: ['Security built in,', 'not bolted on.'],
|
||||||
p1: 'Infrastructure is software now. Private 5G networks, industrial control systems and cloud platforms are assembled from the code and components of many vendors. Every interface between them is an attack surface, and off-the-shelf tools were not built for that.',
|
p1: 'Infrastructure is software now. Private 5G networks, industrial control systems and cloud platforms are assembled from the code and components of many vendors, and every interface between them is an attack surface that off-the-shelf tools were never designed to cover.',
|
||||||
p2: 'CipherCell is a security engineering company for software. We model real adversaries, build zero trust into the architecture rather than around it, and prove that the code holds. From the first threat model to the running system.',
|
p2: 'CipherCell is a security engineering company built by specialists in networks and software. We take on the hard problems, whatever the vertical, and solve them with security at the core of every decision.',
|
||||||
cta: 'About CipherCell',
|
cta: 'About CipherCell',
|
||||||
},
|
},
|
||||||
method: {
|
method: {
|
||||||
@@ -278,11 +278,6 @@ const en = {
|
|||||||
failed: 'Registration did not go through. Please try again, or write to office@ciphercell.de.',
|
failed: 'Registration did not go through. Please try again, or write to office@ciphercell.de.',
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
confirm: {
|
|
||||||
title: 'Confirm your registration.',
|
|
||||||
body: 'One click, and your address is registered for the CipherVerifier alpha. One email when it is released. Nothing else.',
|
|
||||||
submit: 'Confirm registration',
|
|
||||||
},
|
|
||||||
confirmed: {
|
confirmed: {
|
||||||
title: 'Registered.',
|
title: 'Registered.',
|
||||||
body: 'Your address is confirmed. You will hear from us once, when the CipherVerifier alpha is released.',
|
body: 'Your address is confirmed. You will hear from us once, when the CipherVerifier alpha is released.',
|
||||||
@@ -404,8 +399,8 @@ const de: typeof en = {
|
|||||||
},
|
},
|
||||||
manifesto: {
|
manifesto: {
|
||||||
lines: ['Sicherheit eingebaut,', 'nicht nachträglich angefügt.'],
|
lines: ['Sicherheit eingebaut,', 'nicht nachträglich angefügt.'],
|
||||||
p1: 'Infrastruktur ist heute Software. Private 5G-Netze, industrielle Steuerungssysteme und Cloud-Plattformen werden aus Code und Komponenten vieler Hersteller zusammengesetzt. Jede Schnittstelle dazwischen ist eine Angriffsfläche, und Standardwerkzeuge sind dafür nicht gebaut.',
|
p1: 'Infrastruktur ist heute Software. Private 5G-Netze, industrielle Steuerungssysteme und Cloud-Plattformen setzen sich aus Code und Komponenten vieler Hersteller zusammen und jede Schnittstelle dazwischen ist eine Angriffsfläche, für die Lösungen von der Stange nie gedacht waren.',
|
||||||
p2: 'CipherCell ist ein Security-Engineering-Unternehmen für Software. Wir modellieren reale Angreifer, verankern Zero Trust in der Architektur statt daneben und weisen nach, dass der Code standhält. Vom ersten Bedrohungsmodell bis zum laufenden System.',
|
p2: 'CipherCell ist ein Security-Engineering-Unternehmen von Spezialisten für Netzwerke und Software. Wir packen die schwierigen Probleme an, ganz gleich in welcher Branche, und machen Sicherheit zum Kern jeder Entscheidung.',
|
||||||
cta: 'Über CipherCell',
|
cta: 'Über CipherCell',
|
||||||
},
|
},
|
||||||
method: {
|
method: {
|
||||||
@@ -628,11 +623,6 @@ const de: typeof en = {
|
|||||||
failed: 'Die Registrierung ist nicht angekommen. Bitte versuchen Sie es erneut oder schreiben Sie an office@ciphercell.de.',
|
failed: 'Die Registrierung ist nicht angekommen. Bitte versuchen Sie es erneut oder schreiben Sie an office@ciphercell.de.',
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
confirm: {
|
|
||||||
title: 'Registrierung bestätigen.',
|
|
||||||
body: 'Ein Klick, und Ihre Adresse ist für die Alpha von CipherVerifier registriert. Eine E-Mail, sobald sie erscheint. Sonst nichts.',
|
|
||||||
submit: 'Registrierung bestätigen',
|
|
||||||
},
|
|
||||||
confirmed: {
|
confirmed: {
|
||||||
title: 'Registriert.',
|
title: 'Registriert.',
|
||||||
body: 'Ihre Adresse ist bestätigt. Sie hören einmal von uns, sobald die Alpha von CipherVerifier erscheint.',
|
body: 'Ihre Adresse ist bestätigt. Sie hören einmal von uns, sobald die Alpha von CipherVerifier erscheint.',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Base from '../layouts/Base.astro';
|
import Base from '../layouts/Base.astro';
|
||||||
import { localePath, t, type Lang } from '../i18n/ui';
|
import { localePath, t, type Lang } from '../i18n/ui';
|
||||||
interface Props { lang: Lang; page: 'confirm' | 'confirmed' | 'error' }
|
interface Props { lang: Lang; page: 'confirmed' | 'error' }
|
||||||
const { lang, page } = Astro.props;
|
const { lang, page } = Astro.props;
|
||||||
const p = t(lang).verifier.soon.notify.pages;
|
const p = t(lang).verifier.soon.notify.pages;
|
||||||
const r = p[page];
|
const r = p[page];
|
||||||
@@ -13,28 +13,12 @@ const product = localePath(lang, '/cipherverifier');
|
|||||||
<h1 class="display result__title">{r.title}</h1>
|
<h1 class="display result__title">{r.title}</h1>
|
||||||
<div class="result__body">
|
<div class="result__body">
|
||||||
<p class="muted">{r.body}</p>
|
<p class="muted">{r.body}</p>
|
||||||
{page === 'confirm' ? (
|
<a class="link" href={page === 'error' ? `${product}#notify` : product}>{r.link}</a>
|
||||||
// The token travels in the link's query; the script copies it into the post. The
|
|
||||||
// button, not the link, confirms: mail scanners open links but do not press buttons.
|
|
||||||
<form action="/notify/verify" method="post" data-confirm>
|
|
||||||
<input type="hidden" name="lang" value={lang} />
|
|
||||||
<input type="hidden" name="t" value="" />
|
|
||||||
<button class="button" type="submit">{p.confirm.submit}</button>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<a class="link" href={page === 'error' ? `${product}#notify` : product}>{'link' in r ? r.link : ''}</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Base>
|
</Base>
|
||||||
|
|
||||||
<script>
|
|
||||||
const form = document.querySelector<HTMLFormElement>('[data-confirm]');
|
|
||||||
const field = form?.querySelector<HTMLInputElement>('[name="t"]');
|
|
||||||
if (field) field.value = new URLSearchParams(location.search).get('t') ?? '';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.result__title { grid-column: 1 / -1; }
|
.result__title { grid-column: 1 / -1; }
|
||||||
.result__body { grid-column: 1 / -1; display: grid; gap: var(--space-5); max-width: var(--measure); justify-items: start; }
|
.result__body { grid-column: 1 / -1; display: grid; gap: var(--space-5); max-width: var(--measure); justify-items: start; }
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
import NotifyPage from '../../_NotifyPage.astro';
|
|
||||||
---
|
|
||||||
<NotifyPage lang="de" page="confirm" />
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
import NotifyPage from '../_NotifyPage.astro';
|
|
||||||
---
|
|
||||||
<NotifyPage lang="en" page="confirm" />
|
|
||||||
Reference in new issue
Block a user