diff --git a/.editorconfig b/.editorconfig index ad70dbfd..736b416b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -31,3 +31,8 @@ indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + +[{*.caddy,*.example-caddy,Caddyfile}] +charset = utf-8 +indent_style = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 03882f4d..e94521e7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ site !/static/i18n/*.en.* /theme/overrides/* !/theme/overrides/*.en.* + # commit social card fonts to repo # see: https://github.com/squidfunk/mkdocs-material/issues/6983 # ridiculous hide-and-seek https://stackoverflow.com/a/72380673 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 07f7ef05..14a7c7a1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -23,6 +23,7 @@ "EditorConfig.EditorConfig", "DavidAnson.vscode-markdownlint", "wholroyd.jinja", - "mikestead.dotenv" + "mikestead.dotenv", + "matthewpi.caddyfile-support" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a8b0dfc..1a1f4de9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,13 @@ "[markdown]": { "editor.unicodeHighlight.ambiguousCharacters": true, "editor.unicodeHighlight.invisibleCharacters": true + }, + "[caddyfile]": { + "editor.defaultFormatter": "matthewpi.caddyfile-support", + "editor.formatOnSave": true + }, + "files.associations": { + "*.caddy": "caddyfile", + "*.example-caddy": "caddyfile" } } diff --git a/config/caddy/Caddyfile b/config/caddy/Caddyfile new file mode 100644 index 00000000..bc5f9fc1 --- /dev/null +++ b/config/caddy/Caddyfile @@ -0,0 +1,50 @@ +(pg-umami-config) { + umami { + event_endpoint https://stats.jonaharagon.net/api/send + website_uuid 30b92047-7cbb-4800-9815-2e075a293e0a + # bit of a hack to get umami working properly, nothing to do with cloudflare + client_ip_header CF-Connecting-IP + trusted_ip_header X-Real-IP + cookie_consent umami + cookie_resolution resolution + debug + } +} + +www.privacyguides.org { + import vars + import common/*.caddy + import production/*.caddy +} + +http://www.xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion { + import vars + import common/*.caddy + import production/minio.caddy +} + +http://*.xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion { + @hostnames header_regexp hostname Host (\S+)\.xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd\.onion + handle @hostnames { + reverse_proxy {re.hostname.1}.privacyguides.org:443 { + header_up Host {re.hostname.1}.privacyguides.org + transport http { + tls + } + } + } +} + +privacyguides.org { + import vars + import production/matrix.caddy + + handle { + import production/https.caddy + redir https://www.privacyguides.org{uri} + } +} + +http://xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion { + redir http://www.xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion{uri} +} diff --git a/config/caddy/README.md b/config/caddy/README.md new file mode 100644 index 00000000..224fde5f --- /dev/null +++ b/config/caddy/README.md @@ -0,0 +1,13 @@ +# Caddy Webserver Config + +Requires a build of Caddy with [jonaharagon/caddy-umami](https://github.com/jonaharagon/caddy-umami) installed. + +## Variables + +These variables are set on the server, and can be accessed like `{vars.variable_name}`: + +- `minio_hostname` +- `pg_minio_bucket` +- `pg_matrix_webserver` +- `pg_umami_website_uuid` +- `umami_hostname` diff --git a/config/caddy/common/00-matchers.caddy b/config/caddy/common/00-matchers.caddy new file mode 100644 index 00000000..4fb8dc92 --- /dev/null +++ b/config/caddy/common/00-matchers.caddy @@ -0,0 +1,34 @@ +@static { + path *.ico *.css *.js *.gif *.webp *.avif *.jpg *.jpeg *.png *.svg *.woff *.woff2 +} + +@en path /en/* +@es path /es/* +@fr path /fr/* +@he path /he/* +@it path /it/* +@nl path /nl/* +@ru path /ru/* +@zh-Hant path /zh-Hant/* + +@es-header { + header Accept-Language es* +} +@fr-header { + header Accept-Language fr* +} +@he-header { + header Accept-Language he* +} +@it-header { + header Accept-Language it* +} +@nl-header { + header Accept-Language nl* +} +@ru-header { + header Accept-Language ru* +} +@zh-Hant-header { + header Accept-Language zh-Hant* +} diff --git a/config/caddy/common/30-errors.caddy b/config/caddy/common/30-errors.caddy new file mode 100644 index 00000000..63dd6203 --- /dev/null +++ b/config/caddy/common/30-errors.caddy @@ -0,0 +1,42 @@ +handle_errors { + @errors `{err.status_code} in [404]` + handle @errors { + handle @es { + try_files /i18n/{err.status_code}.es.html i18n/{err.status_code}.en.html + file_server + } + handle @fr { + try_files i18n/{err.status_code}.fr.html i18n/{err.status_code}.en.html + file_server + } + handle @he { + try_files i18n/{err.status_code}.he.html i18n/{err.status_code}.en.html + file_server + } + handle @it { + try_files i18n/{err.status_code}.it.html i18n/{err.status_code}.en.html + file_server + } + handle @nl { + try_files i18n/{err.status_code}.nl.html i18n/{err.status_code}.en.html + file_server + } + handle @ru { + try_files i18n/{err.status_code}.ru.html i18n/{err.status_code}.en.html + file_server + } + handle @zh-Hant { + try_files i18n/{err.status_code}.zh-Hant.html i18n/{err.status_code}.en.html + file_server + } + handle { + try_files i18n/{err.status_code}.en.html + file_server + } + } + + # Handle all other webserver errors with a simple text response + handle { + respond "{err.status_code} {err.status_text}" + } +} diff --git a/config/caddy/common/30-headers.caddy b/config/caddy/common/30-headers.caddy new file mode 100644 index 00000000..dddda80c --- /dev/null +++ b/config/caddy/common/30-headers.caddy @@ -0,0 +1,16 @@ +header X-Frame-Options SAMEORIGIN +header X-Content-Type-Options nosniff +header X-XSS-Protection 0 + +vars pg_csp_self "https://www.privacyguides.org https://cdn.privacyguides.org 'self'" +# You can check whether a CSP directive will fall back to default-src on MDN. +# Add CSP directives WITH a default-src fallback here: +header +Content-Security-Policy "default-src 'none'; script-src {vars.pg_csp_self} 'unsafe-inline'; style-src {vars.pg_csp_self} 'unsafe-inline'; font-src {vars.pg_csp_self} data:; img-src data: {vars.pg_csp_self}; connect-src https://api.github.com https://*.privacyguides.net {vars.pg_csp_self}; frame-src https://*.privacyguides.net https://snowflake.torproject.org {vars.pg_csp_self}" +# Add CSP directives WITHOUT a default-src fallback here: +header +Content-Security-Policy "form-action 'self'; frame-ancestors 'none'; base-uri 'none'; sandbox allow-scripts allow-popups allow-same-origin;" + +header Permissions-Policy "browsing-topics=(), conversion-measurement=(), interest-cohort=(), accelerometer=(), ambient-light-sensor=(), battery=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), usb=()" + +header Access-Control-Allow-Origin "*" + +header @static Cache-Control max-age=2592000 diff --git a/config/caddy/common/50-redirect.caddy b/config/caddy/common/50-redirect.caddy new file mode 100644 index 00000000..90ed40ec --- /dev/null +++ b/config/caddy/common/50-redirect.caddy @@ -0,0 +1,4 @@ +redir /kb* /en/basics/why-privacy-matters/ +redir /license* https://github.com/privacyguides/privacyguides.org/tree/main/README.md#license +redir /coc* /en/CODE_OF_CONDUCT/ +redir /team* /en/about/ diff --git a/config/caddy/common/55-redirect-lang.caddy b/config/caddy/common/55-redirect-lang.caddy new file mode 100644 index 00000000..f55f8425 --- /dev/null +++ b/config/caddy/common/55-redirect-lang.caddy @@ -0,0 +1,30 @@ +route / { + header Cache-Control no-store + + redir @es-header /es + redir @fr-header /fr + redir @he-header /he + redir @it-header /it + redir @nl-header /nl + redir @ru-header /ru + redir @zh-Hant-header /zh-Hant + + # default case + handle { + redir * /en/ + } +} + +@kb { + path */kb */kb/* +} +route @kb { + redir @es /es/basics/why-privacy-matters/ + redir @fr /fr/basics/why-privacy-matters/ + redir @he /he/basics/why-privacy-matters/ + redir @it /it/basics/why-privacy-matters/ + redir @nl /nl/basics/why-privacy-matters/ + redir @ru /ru/basics/why-privacy-matters/ + redir @zh-Hant /zh-Hant/basics/why-privacy-matters/ + redir * /en/basics/why-privacy-matters/ +} diff --git a/config/caddy/common/55-redirect-outdated.caddy b/config/caddy/common/55-redirect-outdated.caddy new file mode 100644 index 00000000..d83d8821 --- /dev/null +++ b/config/caddy/common/55-redirect-outdated.caddy @@ -0,0 +1,50 @@ +redir /browsers /en/desktop-browsers/ +redir /blog https://blog.privacyguides.org +redir /basics/dns-overview /en/advanced/dns-overview/ +redir /basics/tor-overview /en/advanced/tor-overview/ +redir /real-time-communication/communication-network-types /en/advanced/communication-network-types +redir /advanced/real-time-communication /en/advanced/communication-network-types +redir /android/overview /en/os/android-overview/ +redir /linux-desktop/overview /en/os/linux-overview/ +redir /android/grapheneos-vs-calyxos https://blog.privacyguides.org/2022/04/21/grapheneos-or-calyxos/ +redir /ios/configuration https://blog.privacyguides.org/2022/10/22/ios-configuration-guide/ +redir /linux-desktop/hardening https://blog.privacyguides.org/2022/04/22/linux-system-hardening/ +redir /linux-desktop/sandboxing https://blog.privacyguides.org/2022/04/22/linux-application-sandboxing/ +redir /advanced/signal-configuration-hardening https://blog.privacyguides.org/2022/07/07/signal-configuration-and-hardening/ +redir /real-time-communication/signal-configuration-hardening https://blog.privacyguides.org/2022/07/07/signal-configuration-and-hardening/ +redir /advanced/integrating-metadata-removal https://blog.privacyguides.org/2022/04/09/integrating-metadata-removal/ +redir /advanced/erasing-data https://blog.privacyguides.org/2022/05/25/secure-data-erasure/ +redir /operating-systems /en/desktop/ +redir /threat-modeling /en/basics/threat-modeling/ +redir /self-contained-networks /en/tor/ +redir /privacy-policy /en/about/privacy-policy/ +redir /metadata-removal-tools /en/data-redaction/ +redir /basics /en/kb +redir /software/file-encryption /en/encryption/ +redir /providers /en/tools/#service-providers +redir /software/calendar-contacts /en/calendar/ +redir /calendar-contacts /en/calendar/ +redir /software/metadata-removal-tools /en/data-redaction/ +redir /contact /en/about/ +redir /welcome-to-privacy-guides https://blog.privacyguides.org/2021/09/14/welcome-to-privacy-guides/ +redir /software/email /en/email-clients/ +redir /providers/paste /en/tools/ +redir /blog/2019/10/05/understanding-vpns https://www.jonaharagon.com/posts/understanding-vpns/ +redir /terms-and-notices /en/about/notices/ +redir /software/networks /en/tor/ +redir /social-news-aggregator /en/news-aggregators/ +redir /basics/erasing-data https://blog.privacyguides.org/2022/05/25/secure-data-erasure/ +redir /linux-desktop /en/desktop/ + +handle_path /providers/* { + redir * /en/{uri} +} +handle_path /software/* { + redir * /en/{uri} +} +handle_path /blog/* { + redir * https://blog.privacyguides.org/{uri} +} +handle_path /assets/* { + redir * /en/assets/{uri} +} diff --git a/config/caddy/common/80-canonical.caddy b/config/caddy/common/80-canonical.caddy new file mode 100644 index 00000000..f41e0b4f --- /dev/null +++ b/config/caddy/common/80-canonical.caddy @@ -0,0 +1,6 @@ +@canonicalPath { + path */ +} +route @canonicalPath { + rewrite @canonicalPath {http.request.orig_uri.path}index.html +} diff --git a/config/caddy/production/https.caddy b/config/caddy/production/https.caddy new file mode 100644 index 00000000..12d75208 --- /dev/null +++ b/config/caddy/production/https.caddy @@ -0,0 +1,2 @@ +header ?Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" +header +Content-Security-Policy upgrade-insecure-requests; diff --git a/config/caddy/production/matrix.caddy b/config/caddy/production/matrix.caddy new file mode 100644 index 00000000..77dc6abb --- /dev/null +++ b/config/caddy/production/matrix.caddy @@ -0,0 +1,13 @@ +@matrix { + path /.well-known/matrix/* +} + +handle @matrix { + reverse_proxy 10.163.5.51:81 { + header_up Host matrix.privacyguides.org + header_up X-Forwarded-Port {http.request.port} + header_up X-Forwarded-TlsProto {tls_protocol} + header_up X-Forwarded-TlsCipher {tls_cipher} + header_up X-Forwarded-HttpsProto {proto} + } +} diff --git a/config/caddy/production/minio.caddy b/config/caddy/production/minio.caddy new file mode 100644 index 00000000..2268ca18 --- /dev/null +++ b/config/caddy/production/minio.caddy @@ -0,0 +1,31 @@ +cache +encode zstd gzip +reverse_proxy http://10.163.3.10:9000 { + header_up Host privacyguides-org-production.stor1-minio.jonaharagon.net + header_down -Server + header_down -Vary + header_down -X-* + + @200ok status 2xx 304 + handle_response @200ok { + import pg-umami-config + copy_response + copy_response_headers + } + + @error404 status 404 + handle_response @error404 { + @addSlash { + expression !{path}.endsWith("/") + } + redir @addSlash {http.request.orig_uri.path}/ + } + + @error400 status 400 + handle_response @error400 { + @real404 { + path *//index.html + } + respond @real404 404 + } +} diff --git a/config/mkdocs-common.yml b/config/mkdocs-common.yml index 65d7dd4b..bc7aac19 100644 --- a/config/mkdocs-common.yml +++ b/config/mkdocs-common.yml @@ -84,10 +84,23 @@ extra: link: /ru/ lang: ru icon: https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg/1f1f7-1f1fa.svg + consent: + title: !ENV [ANALYTICS_CONSENT_TITLE, "Contribute anonymous statistics"] + description: !ENV [ANALYTICS_CONSENT_BODY, "We use cookies to collect anonymous usage statistics. You can opt out if you wish."] + cookies: + umami: + name: Self-Hosted Analytics + checked: true + github: + name: GitHub + checked: false + actions: + - reject + - accept + - manage repo_url: https://github.com/privacyguides/privacyguides.org repo_name: "" -edit_uri: edit/main/docs/ theme: name: material @@ -135,6 +148,7 @@ extra_css: extra_javascript: - assets/javascripts/mathjax.js - assets/javascripts/randomize-element.js + - assets/javascripts/resolution.js watch: - ../theme @@ -270,6 +284,7 @@ nav: - 'about/criteria.md' - 'about/notices.md' - 'about/privacy-policy.md' + - 'about/statistics.md' - !ENV [NAV_COMMUNITY, 'Community']: - 'about/donate.md' - !ENV [NAV_ONLINE_SERVICES, 'Online Services']: 'about/services.md' diff --git a/docs/about/privacy-policy.md b/docs/about/privacy-policy.md index 8f42884f..47677953 100644 --- a/docs/about/privacy-policy.md +++ b/docs/about/privacy-policy.md @@ -7,13 +7,18 @@ Privacy Guides is a community project operated by a number of active volunteer c The privacy of our website visitors is important to us, so we do not track any individual people. As a visitor to our website: -- No personal information is collected -- No information such as cookies are stored in the browser +- No personal information is stored - No information is shared with, sent to or sold to third-parties - No information is shared with advertising companies - No information is mined and harvested for personal and behavioral trends - No information is monetized +You can view the data we collect on our [statistics](statistics.md) page. + +We run a self-hosted installation of [Umami](https://umami.is) to collect some anonymous usage data for statistical purposes. The goal is to track overall trends in our website traffic, it is not to track individual visitors. All the data is in aggregate only, and no personal data is stored. + +The only data which is collected is data sent in a standard web request, which includes referral sources, the page you're visiting, your user agent, your IP address, and your screen resolution. The raw data is immediately discarded after statistics have been generated, for example if we collect your screen resolution as `1125x2436`, the only data we retain is "mobile device" and not your specific resolution. + ## Data We Collect From Account Holders If you register for an account on one of our services, we may collect any information you provide us (such as your email, password, profile information, etc.) and use that information to provide you with the service. We never share or sell this data. diff --git a/docs/about/statistics.md b/docs/about/statistics.md new file mode 100644 index 00000000..aebeeee3 --- /dev/null +++ b/docs/about/statistics.md @@ -0,0 +1,14 @@ +--- +title: Traffic Statistics +--- + +We self-host [Umami](https://umami.is) to create a nice visualization of our traffic statistics, which are public at the link below. With this process: + +- Your information is never shared with a third-party, it stays on servers we control +- Your personal data is never saved, we only collect data in aggregate +- No client-side JavaScript is required + +Because of these facts, keep in mind our statistics may be inaccurate. It is a useful tool to compare different dates with each other and analyze overall trends, but the actual numbers may be far off from reality. They're *precise* statistics, but not *accurate* statistics. + +[View Statistics](https://stats.privacyguides.net/share/nVWjyd2QfgOPBhMF/www.privacyguides.org){ .md-button .md-button--primary } +[Opt-Out](#__consent){ .md-button } diff --git a/includes/strings.en.env b/includes/strings.en.env index bfdeb8d7..bfad2491 100644 --- a/includes/strings.en.env +++ b/includes/strings.en.env @@ -1,13 +1,16 @@ +ANALYTICS_CONSENT_BODY="We collect anonymous statistics about your visits to help us improve the site. We do not track you across other websites. If you disable this, we will not know when you have visited our site. We will save a single cookie in your browser to remember your preference." +ANALYTICS_CONSENT_TITLE="Contribute anonymous statistics" LANG="English" LANG_ENGLISH="English" SITE_NAME="Privacy Guides" SITE_DESCRIPTION="Privacy Guides is your central privacy and security resource to protect yourself online." FOOTER_COPYRIGHT_INTRO="Privacy Guides is a non-profit, socially motivated website that provides information for protecting your data security and privacy." FOOTER_COPYRIGHT_AFFILIATE="We do not make money from recommending certain products, and we do not use affiliate links." +FOOTER_COPYRIGHT_ANALYTICS="Anonymous statistics preferences." FOOTER_COPYRIGHT_DATE="2019 - 2024 Privacy Guides and contributors." FOOTER_COPYRIGHT_ICON='' FOOTER_COPYRIGHT_LICENSE="Content license:" -FOOTER_COPYRIGHT="$FOOTER_COPYRIGHT_INTRO
$FOOTER_COPYRIGHT_AFFILIATE
© $FOOTER_COPYRIGHT_DATE $FOOTER_COPYRIGHT_ICON $FOOTER_COPYRIGHT_LICENSE CC BY-ND 4.0." +FOOTER_COPYRIGHT="$FOOTER_COPYRIGHT_INTRO
$FOOTER_COPYRIGHT_AFFILIATE
© $FOOTER_COPYRIGHT_DATE $FOOTER_COPYRIGHT_ICON $FOOTER_COPYRIGHT_LICENSE CC BY-ND 4.0. $FOOTER_COPYRIGHT_ANALYTICS" THEME_LIGHT="Switch to light mode" THEME_DARK="Switch to dark mode" THEME_AUTO="Switch to system theme" diff --git a/theme/assets/javascripts/resolution.js b/theme/assets/javascripts/resolution.js new file mode 100644 index 00000000..6bdd779b --- /dev/null +++ b/theme/assets/javascripts/resolution.js @@ -0,0 +1,78 @@ +function setCookie(cname, cvalue, exdays) { + const d = new Date(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + let expires = "expires="+d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function getCookie(cname) { + let name = cname + "="; + let ca = document.cookie.split(';'); + for(let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} + +var consent = __md_get("__consent") +if (!consent) { + __md_set("__consent", {umami: true}); + if (getCookie('resolution') == '') { + const resolution = `${window.screen.width}x${window.screen.height}`; + setCookie('resolution', resolution, 30); + } +} + +if (consent && consent.umami) { + if (getCookie('resolution') == '') { + const resolution = `${window.screen.width}x${window.screen.height}`; + setCookie('resolution', resolution, 30); + } + setCookie('umami', 'true', 0); +} else { + setCookie('umami', 'false', 365); + setCookie('resolution', "0x0", 0); +} + +var consent = __md_get("__consent") +if (consent) { + for (var input of document.forms.consent.elements) + if (input.name) + input.checked = consent[input.name] || false + +/* Show consent with a small delay, but not if browsing locally */ +} else if (location.protocol !== "file:") { +setTimeout(function() { + var el = document.querySelector("[data-md-component=consent]") + el.hidden = false +}, 250) +} + +/* Intercept submission of consent form */ +var form = document.forms.consent +for (var action of ["submit", "reset"]) +form.addEventListener(action, function(ev) { + ev.preventDefault() + + /* Reject all cookies */ + if (ev.type === "reset") + for (var input of document.forms.consent.elements) + if (input.name) + input.checked = false + + /* Grab and serialize form data */ + __md_set("__consent", Object.fromEntries( + Array.from(new FormData(form).keys()) + .map(function(key) { return [key, true] }) + )) + + /* Remove anchor to omit consent from reappearing and reload */ + location.hash = ''; + location.reload() +}) diff --git a/theme/partials/javascripts/consent.html b/theme/partials/javascripts/consent.html new file mode 100644 index 00000000..106c22c2 --- /dev/null +++ b/theme/partials/javascripts/consent.html @@ -0,0 +1 @@ +