diff --git a/public/.well-known/security.txt b/public/.well-known/security.txt index 905625d..c52a190 100644 --- a/public/.well-known/security.txt +++ b/public/.well-known/security.txt @@ -2,5 +2,5 @@ Contact: mailto:riric65@protonmail.com Expires: 2027-02-14T00:00:00.000Z Preferred-Languages: fr, en -Canonical: https://zektyc.duckdns.org/.well-known/security.txt -Policy: https://zektyc.duckdns.org/tos +Canonical: https://riricdev.tail5ea5cd.ts.net/.well-known/security.txt +Policy: https://riricdev.tail5ea5cd.ts.net/fr/tos diff --git a/public/blog/en/index.html b/public/blog/en/index.html index 1f21817..110046b 100644 --- a/public/blog/en/index.html +++ b/public/blog/en/index.html @@ -7,6 +7,9 @@ Blog · Zektyc + + + diff --git a/public/blog/index.html b/public/blog/index.html index 31f7143..8868c74 100644 --- a/public/blog/index.html +++ b/public/blog/index.html @@ -6,13 +6,16 @@ Blog · Zektyc - + + + + - + diff --git a/public/en/index.html b/public/en/index.html new file mode 100644 index 0000000..b2b3669 --- /dev/null +++ b/public/en/index.html @@ -0,0 +1,266 @@ + + + + + + + + + Zektyc — Privacy and security, from France + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Based in France · Independent

+

+ Privacy is not a luxury.
+ Neither is security. +

+

Zektyc helps people take back control of their data, calmly. We fight for privacy and security — no jargon, no panic.

+ +
+ +
+

Why we do this

+

Surveillance is everywhere. Every click is tracked, every message analyzed, every bit of data resold. Privacy is not a luxury: it's a condition of freedom. We believe your data should belong to you — to you, and no one else. We explain, we take the drama out of it, and we hand you concrete tools to take back control, from a simple browser to a personal server. No jargon, no panic: our job is to set things straight.

+
+ +
+

Our projects

+

We don't just talk about protection, we put it into practice. Every project is self-hosted and collects no data.

+
+
+

Fingerprint Test

+

Find out how identifiable your browser makes you, with a score and explanations. Everything is computed locally, nothing is sent or stored.

+ Test my fingerprint +
+ +
+
+ +
+

Our principles

+
+
+

Privacy

+

Your data stays yours, period. We stand against trackers, surveillance and mass data collection.

+
+
+

Security

+

Concrete protections, tested and explained simply. Because security is for everyone, not just the experts.

+
+
+

Transparency

+

We do what we say, and we say what we do. No vague promises, no black magic.

+
+
+

Made in France

+

We're based in France and proud of it. Local, independent, human.

+
+
+
+ +
+

+ Disponible sur Tor · + l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion +

+
+ +
+

Frequently asked questions

+

A few questions we get asked a lot, with clear answers. The basic principle: no collection, no tracking, no compromise.

+
+
+

Does this website collect my data?

+

No. Zektyc intentionally collects nothing: no account, no form, no cookie, no tracker, no analytics and no share buttons. The only data processed is automatic technical logging (IP address, date and time, requested resource), kept for fourteen days maximum for security purposes, then automatically deleted. Our privacy policy details all of these processing activities.

+
+
+

Why are there no cookies on this website?

+

Because tracking is never necessary for an informational website: it only serves to profile visitors and resell their data. Without cookies, there is no tracking, no targeted advertising and no consent to extort. It is our way of practising what we preach: privacy is not a luxury, it is a fundamental right, and it starts with technical sobriety.

+
+
+

Is online privacy really for everyone?

+

Yes, and that is precisely why Zektyc exists. Security is not reserved for experts: everyone can make simple, effective choices, such as using a privacy-friendly browser, choosing strong passwords, enabling two-factor authentication and letting a password manager handle the rest. Our job is to help you put these protections in place without jargon and without scaring you.

+
+
+

What does Zektyc actually do?

+

We help people take back control of their data, calmly. We explain real threats simply, without panic or sensationalism, we recommend concrete protections that are tested and easy to adopt, and we put privacy and security back at the centre of technical decisions. We are based in France, independent and human, and we practise what we advise.

+
+
+

Is the website secure?

+

Yes. The website is only reachable over HTTPS, with restrictive security headers, rate limiting per IP address and blocking of sensitive paths. If you find a flaw, you can report it through our responsible disclosure policy, published in the standard /.well-known/security.txt file, without legal risk as long as you respect our reporting conditions.

+
+
+

What do you do with the messages I send you?

+

Your messages are used solely to answer your request. They are never shared, never used for marketing and not kept beyond the time needed for the exchange. Messages transit through our messaging providers (Proton Mail, Signal, Discord), which process them only to deliver them; Signal encrypts your exchanges end to end.

+
+
+

How can I contact you?

+

By e-mail at riric65@protonmail.com, by Signal at the username notriric.05, or through our official Discord server. For any sensitive or confidential communication, prefer e-mail or Signal over Discord, as these channels respect your privacy more. We reply within a reasonable time, without canned phrases.

+
+
+

Is this website free?

+

Yes. Browsing the website is entirely free and will remain so. We do not sell your data, we display no advertising and we claim nothing but your trust. If we ever offer paid services, they will be clearly presented and kept separate from the free information we publish.

+
+
+
+ +
+

Want to talk?

+

A project, a question, a concern? Drop us a line — we're happy to answer, without canned phrases.

+
+
+ Signal + notriric.05 +
+
+ Discord + discord.gg/mAXeNf7zUV + +
+ +
+
+
+ + + + diff --git a/public/equipe/en/index.html b/public/equipe/en/index.html index bc1c9f0..2b9aaad 100644 --- a/public/equipe/en/index.html +++ b/public/equipe/en/index.html @@ -7,6 +7,9 @@ Team · Zektyc + + + diff --git a/public/equipe/index.html b/public/equipe/index.html index b9d84f4..42a1f47 100644 --- a/public/equipe/index.html +++ b/public/equipe/index.html @@ -6,13 +6,16 @@ Équipe · Zektyc - + + + + - + diff --git a/public/fingerprint-test/en/index.html b/public/fingerprint-test/en/index.html index 8cb60b3..2640265 100644 --- a/public/fingerprint-test/en/index.html +++ b/public/fingerprint-test/en/index.html @@ -10,6 +10,9 @@ Browser Fingerprint Test · Zektyc + + + diff --git a/public/fingerprint-test/index.html b/public/fingerprint-test/index.html index 8e5015a..99a8c3a 100644 --- a/public/fingerprint-test/index.html +++ b/public/fingerprint-test/index.html @@ -10,6 +10,9 @@ Test d'empreinte numérique · Zektyc + + + diff --git a/public/index.html b/public/index.html index 26cf104..e55dac5 100644 --- a/public/index.html +++ b/public/index.html @@ -10,7 +10,7 @@ Zektyc — Vie privée et sécurité, depuis la France - + @@ -22,13 +22,13 @@ property="og:description" content="Zektyc défend la vie privée et la sécurité, depuis la France. Sans jargon, sans panique." /> - + - + - + + + +

Redirection… / Redirecting…

+ +` +} + function bearerToken(req: Request): string | null { const h = req.headers.get("authorization") return h ? (h.match(/^Bearer\s+(\S+)$/i)?.[1] ?? null) : null @@ -533,20 +575,20 @@ function errorPage(status: number, req?: Request): Response { .replace(/__MSG__/g, isFr ? "La page que vous recherchez n\u2019existe pas, a \u00e9t\u00e9 d\u00e9plac\u00e9e ou une erreur s\u2019est produite." : "The page you are looking for does not exist, has been moved, or an error occurred.") - .replace(/__HREF__/g, `/${isFr ? "" : "en/"}`) + .replace(/__HREF__/g, `/${isFr ? "fr" : "en"}/`) .replace(/__BACK__/g, isFr ? "Retour \u00e0 l\u2019accueil" : "Back to home") - .replace(/__NEWS_HREF__/g, isFr ? "blog/" : "en/blog/") + .replace(/__NEWS_HREF__/g, isFr ? "fr/blog/" : "en/blog/") .replace(/__NEWS__/g, isFr ? "Blog" : "Blog") - .replace(/__PROJECTS_HREF__/g, isFr ? "projets/" : "en/projets/") + .replace(/__PROJECTS_HREF__/g, isFr ? "fr/projets/" : "en/projets/") .replace(/__PROJECTS__/g, isFr ? "Projets" : "Projects") .replace(/__FR_ACTIVE__/g, isFr ? "active" : "") .replace(/__EN_ACTIVE__/g, !isFr ? "active" : "") .replace(/__FR_LABEL__/g, isFr ? "FR" : "FR") .replace(/__EN_LABEL__/g, !isFr ? "EN" : "EN") .replace(/__NOTE__/g, isFr ? "Ce site ne collecte rien. Aucun cookie, aucun tracker." : "This site collects nothing. No cookies, no trackers.") - .replace(/__TOS_HREF__/g, isFr ? "/tos" : "/en/tos") + .replace(/__TOS_HREF__/g, isFr ? "/fr/tos" : "/en/tos") .replace(/__TOS__/g, isFr ? "Conditions d\u2019utilisation" : "Terms of Service") - .replace(/__PRIV_HREF__/g, isFr ? "/privacy-policy" : "/en/privacy-policy") + .replace(/__PRIV_HREF__/g, isFr ? "/fr/privacy-policy" : "/en/privacy-policy") .replace(/__PRIV__/g, isFr ? "Confidentialit\u00e9" : "Privacy") return new Response(body, { @@ -682,12 +724,16 @@ function newsIndex(lang: "fr" | "en"): string { function writeSitemaps(articles: Article[]) { const base: { fr: string; en: string; lm: string }[] = [ - { fr: "/fr/", en: "/en/", lm: "2026-08-14" }, + { fr: "/fr/", en: "/en/", lm: "2026-09-06" }, { fr: "/fr/tos", en: "/en/tos", lm: "2026-08-14" }, { fr: "/fr/privacy-policy", en: "/en/privacy-policy", lm: "2026-08-14" }, { fr: "/fr/blog/", en: "/en/blog/", lm: "2026-08-14" }, { fr: "/fr/fingerprint-test", en: "/en/fingerprint-test", lm: "2026-08-16" }, { fr: "/fr/projets", en: "/en/projets", lm: "2026-08-17" }, + { fr: "/fr/equipe", en: "/en/equipe", lm: "2026-08-14" }, + { fr: "/fr/zmap", en: "/en/zmap", lm: "2026-08-17" }, + { fr: "/fr/zmap/archives", en: "/en/zmap/archives", lm: "2026-08-17" }, + { fr: "/fr/zmap/vitesses-par-route", en: "/en/zmap/road-speeds", lm: "2026-08-17" }, ] const art = articles.map((a) => ({ fr: `/fr/blog/${a.slug}/`, en: `/en/blog/${a.slug}/`, lm: a.modified || a.published })) const pages = [...base, ...art].sort((x, y) => x.fr.localeCompare(y.fr)) @@ -746,7 +792,7 @@ function clientIp(req: Request): string { const LANG_ROUTES: Record string> = { fr: (rest) => (rest ? "/" + rest : "/"), - en: (rest) => (rest ? "/" + rest.replace(/\/+$/, "") + "/en" : "/"), + en: (rest) => (rest ? "/" + rest.replace(/\/+$/, "") + "/en" : "/en"), } function resolveLangPath(path: string): string | null { @@ -764,6 +810,13 @@ function lookup(path: string) { return resolved } +function isHtmlFile(path: string): boolean { + try { + const resolved = lookup(path) + return !!resolved && resolved.endsWith(".html") && existsSync(resolved) + } catch { return false } +} + function matchArticle(p: string, articles: Article[]): { article: Article; lang: "fr" | "en" } | null { const m = p.match(/^\/blog\/([a-z0-9-]+)(?:\/en)?\/?$/) if (!m) return null @@ -773,6 +826,10 @@ function matchArticle(p: string, articles: Article[]): { article: Article; lang: return article ? { article, lang } : null } +function artPath(matcher: { article: Article; lang: "fr" | "en" }): string { + return `/blog/${matcher.article.slug}` +} + function serveAdminIndex() { const f = file(normalize(join(root, "z-panel", "index.html"))) return f.exists().then((exists) => @@ -1218,13 +1275,29 @@ server = serve({ if (page) return html(page, "public, max-age=300") } - const p = resolveLangPath(pathname) ?? pathname + // Les chemins sans préfixe de langue (/fr/ ou /en/) sont des points d'entrée : + // on ne sert jamais de page ici, on renvoie la page-redirect qui choisit la langue + // (localStorage puis navigateur, sinon fr). Les assets, /api, /docs et le honeypot + // WordPress restent intacts (ils ne se résolvent pas en page HTML). const articles = readArticles() + if (!/^\/(fr|en)(\/|$)/.test(pathname)) { + // Anciennes URLs EN de l'ère « /zmap/en/... » : 301 direct vers /en/zmap/... + const legacyEn = pathname.match(/^\/zmap\/en(?:\/(.*))?$/) + if (legacyEn) { + const sub = legacyEn[1] ? "/" + legacyEn[1].replace(/\/+$/, "") : "" + return new Response(null, { status: 301, headers: { Location: `/en/zmap${sub}/` } }) + } + const artMatch = matchArticle(pathname, articles) + if (artMatch) return html(langRedirectPage(artPath(artMatch)), "no-cache") + if (isHtmlFile(pathname)) return html(langRedirectPage(pathname === "/" ? "/" : pathname), "no-cache") + } + + const p = resolveLangPath(pathname) ?? pathname const matched = matchArticle(p, articles) if (matched) return html(articlePage(matched.article, matched.lang)) - if (p === "/actualites" || p === "/actualites/") return new Response(null, { status: 301, headers: { Location: "/blog/" } }) + if (p === "/actualites" || p === "/actualites/") return new Response(null, { status: 301, headers: { Location: "/fr/blog/" } }) if (p === "/actualites/en" || p === "/actualites/en/") return new Response(null, { status: 301, headers: { Location: "/en/blog/" } }) - if (p.startsWith("/actualites")) return new Response(null, { status: 301, headers: { Location: "/blog/" } }) + if (p.startsWith("/actualites")) return new Response(null, { status: 301, headers: { Location: "/fr/blog/" } }) const filePath = lookup(decodeURIComponent(p)) if (!filePath) return new Response("Forbidden", { status: 403 }) diff --git a/src/zmap-api.ts b/src/zmap-api.ts index 590d750..8cca14c 100644 --- a/src/zmap-api.ts +++ b/src/zmap-api.ts @@ -25,7 +25,7 @@ const RATE_MAX = 30 const RATE_WINDOW = 60_000 const UA = - "ZMapProxy/1.0 (zektyc.duckdns.org; contact: riric65@protonmail.com; OSM proxy)" + "ZMapProxy/1.0 (riricdev.tail5ea5cd.ts.net; contact: riric65@protonmail.com; OSM proxy)" function json(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), {