├── frontend ├── README.md ├── package.json.md5 ├── src │ ├── vite-env.d.ts │ ├── types.ts │ ├── DonateButton │ │ ├── index.css │ │ └── index.tsx │ ├── assets │ │ ├── fonts │ │ │ ├── inter │ │ │ │ ├── Inter-Black.woff2 │ │ │ │ ├── Inter-Bold.woff2 │ │ │ │ ├── Inter-Italic.woff2 │ │ │ │ ├── Inter-Light.woff2 │ │ │ │ ├── Inter-Medium.woff2 │ │ │ │ ├── Inter-Thin.woff2 │ │ │ │ ├── Inter-Regular.woff2 │ │ │ │ ├── Inter-SemiBold.woff2 │ │ │ │ ├── InterVariable.woff2 │ │ │ │ ├── Inter-BlackItalic.woff2 │ │ │ │ ├── Inter-BoldItalic.woff2 │ │ │ │ ├── Inter-ExtraBold.woff2 │ │ │ │ ├── Inter-ExtraLight.woff2 │ │ │ │ ├── Inter-LightItalic.woff2 │ │ │ │ ├── Inter-ThinItalic.woff2 │ │ │ │ ├── Inter-MediumItalic.woff2 │ │ │ │ ├── Inter-ExtraBoldItalic.woff2 │ │ │ │ ├── Inter-ExtraLightItalic.woff2 │ │ │ │ ├── Inter-SemiBoldItalic.woff2 │ │ │ │ └── InterVariable-Italic.woff2 │ │ │ └── TwemojiCountryFlags.woff2 │ │ └── icons │ │ │ ├── osc.svg │ │ │ ├── github.svg │ │ │ ├── bluesky.svg │ │ │ ├── reddit.svg │ │ │ └── discord.svg │ ├── common │ │ ├── toaster.tsx │ │ ├── BrowserLink.tsx │ │ └── ThemeManager.tsx │ ├── FilterLists │ │ ├── CreateFilterList │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── types.ts │ │ ├── ExportFilterList.tsx │ │ ├── ImportFilterList.tsx │ │ └── index.css │ ├── SettingsManager │ │ ├── UninstallCADialog │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── ThemeSelector │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── ExportLogsButton │ │ │ └── index.tsx │ │ ├── ExportDebugDataButton │ │ │ └── index.tsx │ │ ├── PortInput.tsx │ │ ├── LocaleSelector │ │ │ └── index.tsx │ │ ├── IgnoredHostsInput.tsx │ │ ├── AutostartSwitch │ │ │ └── index.tsx │ │ ├── AutoupdateSwitch │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Intro │ │ ├── SettingsScreen │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── WelcomeScreen │ │ │ ├── LocaleList │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── ConnectScreen │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── FilterListsScreen │ │ │ └── index.tsx │ │ └── index.tsx │ ├── constants │ │ └── urls.ts │ ├── RequestLog │ │ └── index.css │ ├── Rules │ │ ├── index.css │ │ └── index.tsx │ ├── components │ │ └── AppHeader │ │ │ ├── index.css │ │ │ └── index.tsx │ ├── ErrorBoundary.tsx │ ├── App.css │ ├── ProxyHotkey.tsx │ ├── style.css │ ├── context │ │ └── ProxyStateContext.tsx │ ├── main.tsx │ ├── StartStopButton.tsx │ ├── i18n │ │ └── index.ts │ └── App.tsx ├── vite.config.ts ├── .prettierrc.json ├── tsconfig.node.json ├── wailsjs │ ├── go │ │ ├── autostart │ │ │ ├── Manager.d.ts │ │ │ └── Manager.js │ │ ├── app │ │ │ ├── App.d.ts │ │ │ └── App.js │ │ ├── models.ts │ │ └── cfg │ │ │ ├── Config.d.ts │ │ │ └── Config.js │ └── runtime │ │ └── package.json ├── index.html ├── Taskfile.yml ├── tsconfig.json ├── .eslintrc.cjs ├── package.json └── i18next-parser.config.js ├── .gitattributes ├── internal ├── sysproxy │ ├── exclusions │ │ ├── .gitignore │ │ ├── windows.txt │ │ ├── README.md │ │ ├── LICENSE │ │ └── common.txt │ ├── system_windows.go │ ├── pac.go │ ├── system_darwin.go │ └── manager.go ├── systray │ ├── logo.ico │ ├── manager_nonwindows.go │ └── manager_windows.go ├── constants │ ├── instanceid_prod.go │ ├── instanceid_nonprod.go │ └── constants.go ├── app │ ├── app_windows.go │ ├── app_nonwindows.go │ ├── wndproc_windows.go │ └── eventshandler.go ├── logger │ ├── redacted_prod.go │ ├── redacted_nonprod.go │ ├── setuplogger_prod.go │ ├── setuplogger_nonprod.go │ ├── README.md │ └── logger.go ├── autostart │ ├── autostart.go │ ├── autostart_windows.go │ ├── autostart_linux.go │ └── autostart_darwin.go ├── cfg │ ├── systemdirs_linux.go │ ├── systemdirs_windows.go │ └── systemdirs_darwin.go └── selfupdate │ ├── executable.go │ └── selfupdate_test.go ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yaml │ └── bug_report.yaml ├── FUNDING.yml ├── pull_request_template.md └── workflows │ ├── release-manifest.yml │ ├── test.yml │ ├── lint.yml │ └── scorecard.yml ├── docs ├── external │ ├── linux-proxy-conf.md │ ├── firefox.png │ ├── how-to-rules.md │ └── known-issues-and-troubleshooting.md └── internal │ ├── useful-articles.md │ ├── requirements.md │ ├── update-checklist.md │ ├── index.md │ ├── filter-lists.md │ ├── project-structure.md │ ├── testing-checklist.md │ └── code-conventions.md ├── assets ├── output.png ├── shield.ico ├── shield.png ├── appicon.png ├── signpath-logo.png └── screenshots │ ├── filter-lists.png │ └── main-window.png ├── scripts ├── .prettierrc.json ├── tsconfig.json └── package.json ├── .gitignore ├── .vscode ├── settings.json └── launch.json ├── CODE_OF_CONDUCT.md ├── wails.json ├── tasks └── build │ ├── Taskfile-windows.yml │ ├── Taskfile-linux.yml │ └── Taskfile-darwin.yml ├── .golangci.yml ├── LICENSE ├── Taskfile.yml ├── SECURITY.md ├── CONTRIBUTING.md ├── main.go └── go.mod /frontend/README.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go text eol=lf 2 | -------------------------------------------------------------------------------- /internal/sysproxy/exclusions/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | a81af967a05f96a3dce12a6b21d9ed51 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /docs/external/linux-proxy-conf.md: -------------------------------------------------------------------------------- 1 | # How to configure proxy on Linux -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ProxyState = 'on' | 'off' | 'loading'; 2 | -------------------------------------------------------------------------------- /assets/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/output.png -------------------------------------------------------------------------------- /assets/shield.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/shield.ico -------------------------------------------------------------------------------- /assets/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/shield.png -------------------------------------------------------------------------------- /assets/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/appicon.png -------------------------------------------------------------------------------- /assets/signpath-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/signpath-logo.png -------------------------------------------------------------------------------- /docs/external/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/docs/external/firefox.png -------------------------------------------------------------------------------- /internal/systray/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/internal/systray/logo.ico -------------------------------------------------------------------------------- /frontend/src/DonateButton/index.css: -------------------------------------------------------------------------------- 1 | .donate-button__icon { 2 | color: rgb(191, 57, 137) !important; 3 | } 4 | -------------------------------------------------------------------------------- /assets/screenshots/filter-lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/screenshots/filter-lists.png -------------------------------------------------------------------------------- /assets/screenshots/main-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/assets/screenshots/main-window.png -------------------------------------------------------------------------------- /internal/constants/instanceid_prod.go: -------------------------------------------------------------------------------- 1 | //go:build prod 2 | 3 | package constants 4 | 5 | const InstanceID = "a7bad8a9-cadb-4ae9-86b3-2e9e81049cb8" 6 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Black.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Bold.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Italic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Light.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Medium.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Thin.woff2 -------------------------------------------------------------------------------- /internal/constants/instanceid_nonprod.go: -------------------------------------------------------------------------------- 1 | //go:build !prod 2 | 3 | package constants 4 | 5 | const InstanceID = "340f21c9-4827-4ba0-9c2b-c4ec2b8b01d5" 6 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/TwemojiCountryFlags.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/TwemojiCountryFlags.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-Regular.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/InterVariable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/InterVariable.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-BlackItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-BoldItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-ExtraBold.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-ExtraLight.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-LightItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-ThinItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-MediumItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-ExtraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-ExtraLightItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/Inter-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/Inter-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/inter/InterVariable-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZenPrivacy/zen-desktop/HEAD/frontend/src/assets/fonts/inter/InterVariable-Italic.woff2 -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | AppName = "Zen" 5 | AppNameLowercase = "zen" 6 | OrgName = "Zen Privacy" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/app/app_windows.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "context" 4 | 5 | func (a *App) Startup(ctx context.Context) { 6 | runShutdownOnWmEndsession(ctx) 7 | a.commonStartup(ctx) 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/common/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayToaster, Position } from '@blueprintjs/core'; 2 | 3 | export const AppToaster = OverlayToaster.create({ 4 | position: Position.TOP, 5 | }); 6 | -------------------------------------------------------------------------------- /internal/app/app_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package app 4 | 5 | import "context" 6 | 7 | func (a *App) Startup(ctx context.Context) { 8 | a.commonStartup(ctx) 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/FilterLists/CreateFilterList/index.css: -------------------------------------------------------------------------------- 1 | .create-filter-list__trusted-label { 2 | display: inline-flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | gap: 4px; 6 | } -------------------------------------------------------------------------------- /frontend/src/SettingsManager/UninstallCADialog/index.css: -------------------------------------------------------------------------------- 1 | .uninstall-ca-dialog { 2 | margin: 0 12px; 3 | } 4 | 5 | .uninstall-ca-dialog__button { 6 | margin-top: 8px; 7 | margin-bottom: 8px; 8 | } -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120, 7 | "useTabs": false, 8 | "endOfLine":"auto" 9 | } 10 | -------------------------------------------------------------------------------- /scripts/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120, 7 | "useTabs": false, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/FilterLists/types.ts: -------------------------------------------------------------------------------- 1 | export enum FilterListType { 2 | GENERAL = 'general', 3 | ADS = 'ads', 4 | PRIVACY = 'privacy', 5 | MALWARE = 'malware', 6 | REGIONAL = 'regional', 7 | CUSTOM = 'custom', 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/bin 2 | build/windows/installer/wails_tools.nsh 3 | node_modules 4 | frontend/dist 5 | .DS_Store 6 | *.dmg 7 | *.app 8 | .env.local 9 | **/coverage/* 10 | **/dist/* 11 | **/tmp/* 12 | .idea 13 | **/*.pprof 14 | **/*.out 15 | -------------------------------------------------------------------------------- /docs/internal/useful-articles.md: -------------------------------------------------------------------------------- 1 | # Useful articles 2 | This is a collection of articles that are useful for understanding the technologies used in this project. 3 | 4 | ## Wails 5 | - [Wails - How does it work?](https://wails.io/docs/howdoesitwork) 6 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": [ 9 | "vite.config.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/Intro/SettingsScreen/index.css: -------------------------------------------------------------------------------- 1 | .settings-card { 2 | margin: 20px 0; 3 | text-align: left; 4 | } 5 | 6 | .settings-divider { 7 | margin: 16px 0 !important; 8 | } 9 | 10 | .settings-note { 11 | margin-top: 20px; 12 | text-align: center; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/autostart/Manager.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | 4 | export function Disable():Promise; 5 | 6 | export function Enable():Promise; 7 | 8 | export function IsEnabled():Promise; 9 | -------------------------------------------------------------------------------- /frontend/src/constants/urls.ts: -------------------------------------------------------------------------------- 1 | export const SOCIAL_LINKS = { 2 | GITHUB: 'https://github.com/ZenPrivacy/zen-desktop', 3 | BLUESKY: 'https://bsky.app/profile/zenprivacy.net', 4 | REDDIT: 'https://www.reddit.com/r/zenprivacy/', 5 | DISCORD: 'https://discord.com/invite/jSzEwby7JY', 6 | OPEN_COLLECTIVE: 'https://opencollective.com/zen-privacy', 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zen 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/Intro/WelcomeScreen/LocaleList/index.css: -------------------------------------------------------------------------------- 1 | .locale-list { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | gap: 10px; 5 | max-width: 500px; 6 | margin: 20px auto; 7 | } 8 | 9 | .locale-content { 10 | display: flex; 11 | align-items: center; 12 | gap: 10px; 13 | } 14 | 15 | .locale-content .locale-radio { 16 | margin: 0; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/RequestLog/index.css: -------------------------------------------------------------------------------- 1 | .request-log__card { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .request-log__empty { 7 | margin-top: .5rem; 8 | font-weight: bold; 9 | } 10 | 11 | .request-log__card__details__value { 12 | line-break: anywhere; 13 | } 14 | 15 | .request-log__card__details__rules td { 16 | line-break: anywhere; 17 | } 18 | -------------------------------------------------------------------------------- /internal/logger/redacted_prod.go: -------------------------------------------------------------------------------- 1 | //go:build prod 2 | 3 | package logger 4 | 5 | // Redacted redacts sensitive data in production logs. 6 | // In non-production environments, it returns the string representation of the input value. 7 | // In a production environment, it always returns the constant "[REDACTED]" to ensure sensitive information is not exposed. 8 | func Redacted(input any) string { 9 | return "[REDACTED]" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[typescriptreact]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[css]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/Intro/WelcomeScreen/index.css: -------------------------------------------------------------------------------- 1 | .welcome-slide { 2 | animation: slide 0.4s ease-out; 3 | min-height: 1.5em; 4 | } 5 | 6 | @keyframes slide { 7 | from { 8 | opacity: 0; 9 | transform: translateY(-8px); 10 | } 11 | to { 12 | opacity: 1; 13 | transform: translateY(0); 14 | } 15 | } 16 | 17 | @media (prefers-reduced-motion: reduce) { 18 | .welcome-slide { 19 | animation: none; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "experimentalDecorators": true, 5 | "module": "Node16", 6 | "lib": ["es2023"], 7 | "outDir": "build", 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "removeComments": true 13 | }, 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["build"] 16 | } 17 | -------------------------------------------------------------------------------- /internal/logger/redacted_nonprod.go: -------------------------------------------------------------------------------- 1 | //go:build !prod 2 | 3 | package logger 4 | 5 | import "fmt" 6 | 7 | // Redacted redacts sensitive data in production logs. 8 | // In non-production environments, it returns the string representation of the input value. 9 | // In a production environment, it always returns the constant "[REDACTED]" to ensure sensitive information is not exposed. 10 | func Redacted(input any) string { 11 | return fmt.Sprint(input) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/Rules/index.css: -------------------------------------------------------------------------------- 1 | .rules { 2 | display: flex; 3 | flex-direction: column; 4 | height: calc(100% - 8px); 5 | } 6 | 7 | .rules__help-button { 8 | margin-bottom: 8px; 9 | } 10 | 11 | .rules__textarea { 12 | font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; 13 | font-size: 0.9em; 14 | height: 100% !important; 15 | } 16 | 17 | .rules__tooltip { 18 | height: 100%; 19 | width: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /internal/autostart/autostart.go: -------------------------------------------------------------------------------- 1 | package autostart 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Manager manages automatic startup of the app on user login. 9 | type Manager struct{} 10 | 11 | // getExecPath returns the path to the currently running executable. 12 | func getExecPath() (string, error) { 13 | execPath, err := os.Executable() 14 | if err != nil { 15 | return "", fmt.Errorf("get executable path: %w", err) 16 | } 17 | return execPath, nil 18 | } 19 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/autostart/Manager.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export function Disable() { 6 | return window['go']['autostart']['Manager']['Disable'](); 7 | } 8 | 9 | export function Enable() { 10 | return window['go']['autostart']['Manager']['Enable'](); 11 | } 12 | 13 | export function IsEnabled() { 14 | return window['go']['autostart']['Manager']['IsEnabled'](); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/AppHeader/index.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | margin: 0 12px; 3 | padding: 6px 0; 4 | border-bottom: 1px dotted #333; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | .heading__branding { 11 | display: flex; 12 | align-items: center; 13 | gap: 6px; 14 | } 15 | 16 | .heading__logo, 17 | .heading__zen { 18 | height: 20px; 19 | width: auto; 20 | fill: rgb(28, 33, 39); 21 | } 22 | 23 | #app.bp5-dark .heading__logo, 24 | #app.bp5-dark .heading__zen { 25 | fill: #fff; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | lint: 5 | desc: Runs eslint. 6 | dir: frontend 7 | cmd: npm run lint 8 | 9 | lint-fix: 10 | desc: Runs eslint --fix (automatically fixes applicable issues). 11 | dir: frontend 12 | cmd: npm run lint -- --fix 13 | 14 | install: 15 | desc: Installs the complete set of dependencies. 16 | dir: frontend 17 | cmd: npm ci 18 | 19 | extract-translations: 20 | desc: Extract i18n translation keys from source code. 21 | dir: frontend 22 | cmd: npm run extract-translations 23 | -------------------------------------------------------------------------------- /docs/internal/requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | - [Go](https://go.dev/dl/) (check the required version in the [go.mod](../../go.mod) file). 3 | - [Node.js + npm](https://nodejs.org/en/download/) (check the required version in the [frontend/package.json](../../frontend/package.json) file). 4 | - [Wails](https://wails.io/docs/gettingstarted/installation). Run `wails doctor` to ensure you have the required dependencies. 5 | - [Task](https://taskfile.dev/installation/). Optional, but recommended for quick access to common tasks. 6 | - [golangci-lint](https://golangci-lint.run/welcome/install/). 7 | -------------------------------------------------------------------------------- /frontend/src/Intro/ConnectScreen/index.css: -------------------------------------------------------------------------------- 1 | .connect-card { 2 | margin: 20px 0; 3 | text-align: center; 4 | } 5 | 6 | .social-links-grid { 7 | display: flex; 8 | flex-direction: column; 9 | gap: 10px; 10 | margin: 20px 0; 11 | } 12 | 13 | .social-row { 14 | display: flex; 15 | gap: 10px; 16 | } 17 | 18 | .social-button { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .social-icon { 25 | width: 20px; 26 | height: 20px; 27 | margin-right: 8px; 28 | vertical-align: middle; 29 | } 30 | 31 | .section-divider { 32 | margin: 16px 0 !important; 33 | } 34 | -------------------------------------------------------------------------------- /docs/internal/update-checklist.md: -------------------------------------------------------------------------------- 1 | # Update Checklist 2 | - [ ] Update the "productVersion" in the wails.json file. 3 | - [ ] Update the [CHANGELOG.md](../../CHANGELOG.md) file with the release notes. 4 | - [ ] Run git tag v and git push --tags to tag the release. 5 | - [ ] Wait for the CI to create a new release on GitHub with the tag and ensure all assets are populated. 6 | - [ ] Mark the release as the latest on GitHub. 7 | 8 | # Release Note format 9 | ## What's New 10 | - **Feature 1**: Feature description. 11 | - **Feature 2**: Feature description. 12 | - Minor improvements and bug fixes. 13 | 14 | Thank you for using Zen! 15 | -------------------------------------------------------------------------------- /frontend/src/DonateButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Icon } from '@blueprintjs/core'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { BrowserOpenURL } from '../../wailsjs/runtime/runtime'; 5 | 6 | import './index.css'; 7 | 8 | const LINK = 'https://opencollective.com/zen-privacy'; 9 | 10 | export function DonateButton() { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /internal/systray/manager_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package systray 4 | 5 | // To be implemented. 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | type Manager struct{} 12 | 13 | func NewManager(string, func(), func()) (*Manager, error) { 14 | return &Manager{}, nil 15 | } 16 | 17 | func (m *Manager) Init(context.Context) error { 18 | return nil 19 | } 20 | 21 | func (m *Manager) Quit() {} 22 | 23 | // OnProxyStarted should be called when the proxy gets started. 24 | func (m *Manager) OnProxyStarted() {} 25 | 26 | // OnProxyStopped should be called when the proxy gets stopped. 27 | func (m *Manager) OnProxyStopped() {} 28 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "type": "module", 4 | "private": true, 5 | "engines": { 6 | "node": ">=20.15.1" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "upload-manifest": "node build/upload-manifest.js" 11 | }, 12 | "devDependencies": { 13 | "@types/html-to-text": "^9.0.4", 14 | "@types/node": "^20.14.10", 15 | "@types/semver": "^7.5.8", 16 | "typescript": "^5.5.3" 17 | }, 18 | "dependencies": { 19 | "@aws-sdk/client-s3": "^3.627.0", 20 | "@octokit/rest": "^21.0.0", 21 | "html-to-text": "^9.0.5", 22 | "marked": "^13.0.2", 23 | "semver": "^7.6.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wailsapp/runtime", 3 | "version": "2.0.0", 4 | "description": "Wails Javascript runtime library", 5 | "main": "runtime.js", 6 | "types": "runtime.d.ts", 7 | "scripts": { 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wailsapp/wails.git" 12 | }, 13 | "keywords": [ 14 | "Wails", 15 | "Javascript", 16 | "Go" 17 | ], 18 | "author": "Lea Anthony ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wailsapp/wails/issues" 22 | }, 23 | "homepage": "https://github.com/wailsapp/wails#readme" 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactNode } from 'react'; 2 | 3 | import { AppToaster } from './common/toaster'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | } 8 | 9 | class ErrorBoundary extends Component { 10 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 11 | AppToaster.show({ 12 | message: `Unexpected error: ${error}`, 13 | intent: 'danger', 14 | }); 15 | console.error('ErrorBoundary', error, errorInfo); 16 | } 17 | 18 | render() { 19 | // eslint-disable-next-line react/destructuring-assignment 20 | return this.props.children; 21 | } 22 | } 23 | 24 | export default ErrorBoundary; 25 | -------------------------------------------------------------------------------- /frontend/src/Intro/index.css: -------------------------------------------------------------------------------- 1 | .intro-screen { 2 | padding-top: 20px; 3 | width: 100%; 4 | animation: reveal 0.3s ease-in; 5 | } 6 | 7 | @media (prefers-reduced-motion: reduce) { 8 | .intro-screen { 9 | animation: none; 10 | } 11 | } 12 | 13 | .intro-heading, 14 | .intro-description { 15 | text-align: center; 16 | } 17 | 18 | .intro-progress-bar { 19 | border-radius: 0 !important; 20 | } 21 | 22 | .intro-progress-bar .bp5-progress-meter { 23 | border-radius: 0 !important; 24 | } 25 | 26 | @keyframes reveal { 27 | from { 28 | opacity: 0; 29 | transform: translateY(0.5rem); 30 | } 31 | to { 32 | opacity: 1; 33 | transform: translateY(0); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project follows the [Zen Privacy Community Code of Conduct](https://github.com/ZenPrivacy/community/blob/main/CODE_OF_CONDUCT.md). 4 | 5 | By participating in this repository and its associated social spaces, you agree to abide by the standards and expectations outlined in the community-wide Code of Conduct. These guidelines are in place to foster a respectful, inclusive, and collaborative environment for everyone involved in Zen Privacy projects. 6 | 7 | If you have any concerns or need to report a violation, please refer to the reporting guidelines provided in the shared document. 8 | 9 | Thank you for helping make this a welcoming space for all. 10 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #app { 2 | height: 100vh; 3 | width: 100vw; 4 | background-color: #f5f8fa; 5 | color: #000; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | #app.bp5-dark { 11 | background: rgb(47, 52, 60); 12 | color: #fff; 13 | } 14 | 15 | .bp5-dark .heading { 16 | border-bottom: 1px dotted #f5f5f5; 17 | } 18 | 19 | /* Light theme version of logo */ 20 | #app:not(.bp5-dark) .heading__logo svg { 21 | stroke: #394b59; 22 | } 23 | 24 | .tabs { 25 | padding: 0 12px; 26 | height: 36px; 27 | flex-shrink: 0; 28 | } 29 | 30 | .content { 31 | overflow-y: auto; 32 | padding: 0 12px; 33 | flex-grow: 1; 34 | } 35 | 36 | .footer { 37 | width: 100vw; 38 | } 39 | -------------------------------------------------------------------------------- /internal/logger/setuplogger_prod.go: -------------------------------------------------------------------------------- 1 | //go:build prod 2 | 3 | package logger 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "path/filepath" 9 | 10 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 11 | "gopkg.in/natefinch/lumberjack.v2" 12 | ) 13 | 14 | // More on the logging setup in README.md. 15 | 16 | func SetupLogger() error { 17 | logsDir, err := getLogsDir(constants.AppName) 18 | if err != nil { 19 | return fmt.Errorf("get logs directory: %w", err) 20 | } 21 | 22 | fileLogger := &lumberjack.Logger{ 23 | Filename: filepath.Join(logsDir, "application.log"), 24 | MaxSize: 5, 25 | MaxBackups: 5, 26 | MaxAge: 1, 27 | Compress: true, 28 | } 29 | 30 | log.SetOutput(fileLogger) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/sysproxy/exclusions/windows.txt: -------------------------------------------------------------------------------- 1 | # This file contains Windows-specific hostnames that Zen will not MITM. 2 | 3 | # Windows Update and Microsoft services 4 | eus-streaming-video-msn.com 5 | akamaized.net 6 | wns.windows.com 7 | live.com 8 | clientconfig.passport.net 9 | wustat.windows.com 10 | windowsupdate.com 11 | msftncsi.com 12 | microsoft.com 13 | msftconnecttest.com 14 | 15 | 16 | # Xbox Live 17 | device.ra.live.com 18 | login.live.com 19 | accounts.live.com 20 | device.auth.xboxlive.com 21 | title.mgt.xboxlive.com 22 | title.auth.xboxlive.com 23 | user.auth.xboxlive.com 24 | xsts.auth.xboxlive.com 25 | dlassets.xboxlive.com 26 | images-eds.xboxlive.com 27 | pf.directory.live.com 28 | privacy.xboxlive.com 29 | profile.xboxlive.com 30 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/app/App.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | import {options} from '../models'; 4 | 5 | export function ExportCustomFilterLists():Promise; 6 | 7 | export function ImportCustomFilterLists():Promise; 8 | 9 | export function IsNoSelfUpdate():Promise; 10 | 11 | export function OnSecondInstanceLaunch(arg1:options.SecondInstanceData):Promise; 12 | 13 | export function OpenLogsDirectory():Promise; 14 | 15 | export function RestartApplication():Promise; 16 | 17 | export function StartProxy():Promise; 18 | 19 | export function StopProxy():Promise; 20 | 21 | export function UninstallCA():Promise; 22 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "Zen", 4 | "outputfilename": "Zen", 5 | "frontend:install": "npm install", 6 | "frontend:build": "npm run build", 7 | "frontend:dev:watcher": "npm run dev", 8 | "frontend:dev:serverUrl": "auto", 9 | "author": { 10 | "name": "Zen Privacy", 11 | "email": "contact@zenprivacy.net" 12 | }, 13 | "info": { 14 | "productName": "Zen", 15 | "productVersion": "0.16.0", 16 | "comments": "Zen is a simple, free and efficient ad-blocker and privacy guard for Windows, macOS and Linux. Visit https://zenprivacy.net for more information.", 17 | "copyright": "Copyright (c) 2025 Zen Privacy Project Developers. Licensed under MIT License." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /internal/logger/setuplogger_nonprod.go: -------------------------------------------------------------------------------- 1 | //go:build !prod 2 | 3 | package logger 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 13 | "gopkg.in/natefinch/lumberjack.v2" 14 | ) 15 | 16 | // More on the logging setup in README.md. 17 | 18 | func SetupLogger() error { 19 | logsDir, err := getLogsDir(constants.AppName) 20 | if err != nil { 21 | return fmt.Errorf("get logs directory: %w", err) 22 | } 23 | 24 | fileLogger := &lumberjack.Logger{ 25 | Filename: filepath.Join(logsDir, "application.log"), 26 | MaxSize: 5, 27 | MaxBackups: 5, 28 | MaxAge: 1, 29 | Compress: true, 30 | } 31 | 32 | log.SetOutput(io.MultiWriter(os.Stdout, fileLogger)) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /docs/internal/index.md: -------------------------------------------------------------------------------- 1 | # Zen: Internal documentation index 2 | ## Getting started 3 | - [Install the dependencies](requirements.md). 4 | - [Optional: Check out the useful articles](useful-articles.md). 5 | - [Get familiarized with the project structure](project-structure.md). 6 | - Run `task -l` to see available tasks. 7 | - Run `task` to automatically install the Go/Node packages and run the app in development mode. 8 | - Get familiarized with the [code conventions followed in the project](code-conventions.md). 9 | - Start exploring the codebase from: 10 | - [The Go entrypoint](/main.go). 11 | - [The frontend entrypoint](/frontend/src/main.tsx). 12 | 13 | ## Development 14 | - [Update checklist](update-checklist.md). Use this to ensure you don't forget to update all necessary parts of the project when making changes. 15 | -------------------------------------------------------------------------------- /frontend/src/common/BrowserLink.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { BrowserOpenURL } from '../../wailsjs/runtime/runtime'; 4 | 5 | export interface BrowserLinkProps { 6 | href: string; 7 | children?: ReactNode; 8 | } 9 | 10 | /** 11 | * An accessible link that opens a URL in the default browser via BrowserOpenURL. 12 | */ 13 | export function BrowserLink({ href, children }: BrowserLinkProps) { 14 | return ( 15 | /* eslint-disable-next-line jsx-a11y/anchor-is-valid */ 16 | BrowserOpenURL(href)} 18 | tabIndex={0} 19 | role="button" 20 | onKeyDown={(e) => { 21 | if (e.key === 'Enter' || e.key === ' ') { 22 | e.preventDefault(); 23 | BrowserOpenURL(href); 24 | } 25 | }} 26 | > 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/ProxyHotkey.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { StartProxy, StopProxy } from '../wailsjs/go/app/App'; 4 | 5 | import { useProxyState } from './context/ProxyStateContext'; 6 | 7 | export function useProxyHotkey(showIntro?: boolean) { 8 | const { proxyState } = useProxyState(); 9 | useEffect(() => { 10 | const spaceDown = (e: KeyboardEvent) => { 11 | if (showIntro) return; 12 | if (e.code === 'Space' && document.activeElement === document.body) { 13 | if (proxyState === 'off') { 14 | StartProxy(); 15 | } else if (proxyState === 'on') { 16 | StopProxy(); 17 | } 18 | } 19 | }; 20 | window.addEventListener('keydown', spaceDown); 21 | return () => window.removeEventListener('keydown', spaceDown); 22 | }, [proxyState, showIntro]); 23 | return null; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/osc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement for Zen 3 | type: Feature 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to suggest a feature in Zen. 9 | Before submitting the issue, please [refer to the project roadmap](https://github.com/ZenPrivacy/zen-desktop/discussions/99) and [search the issue tracker](https://github.com/ZenPrivacy/zen-desktop/issues) to see if it is already being worked on. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description 14 | description: | 15 | Describe the feature you would like to see in Zen. Include a detailed description of why it is needed. 16 | Attach screenshots or videos if it helps explain the feature. 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Wails: Production zen", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "exec", 9 | "program": "${workspaceFolder}/build/bin/zen", 10 | "preLaunchTask": "build", 11 | "cwd": "${workspaceFolder}" 12 | }, 13 | { 14 | "name": "Wails: Debug zen", 15 | "type": "go", 16 | "request": "launch", 17 | "mode": "exec", 18 | "program": "${workspaceFolder}/build/bin/zen", 19 | "preLaunchTask": "build debug", 20 | "cwd": "${workspaceFolder}" 21 | }, 22 | { 23 | "name": "Wails: Dev zen", 24 | "type": "go", 25 | "request": "launch", 26 | "mode": "exec", 27 | "program": "${workspaceFolder}/build/bin/zen", 28 | "preLaunchTask": "build dev", 29 | "cwd": "${workspaceFolder}" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /tasks/build/Taskfile-windows.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | vars: 4 | ARCH64: '{{if eq ARCH "arm"}}arm64{{else}}{{ARCH}}{{end}}' 5 | GIT_TAG: 6 | sh: git describe --tags --always --abbrev=0 7 | 8 | tasks: 9 | prod: 10 | desc: Create a production build of the application. 11 | cmds: 12 | - wails build -o Zen.exe -platform "windows/{{default .ARCH64 .ARCH}}" -nsis -ldflags "-X 'github.com/ZenPrivacy/zen-desktop/internal/cfg.Version={{.GIT_TAG}}'" -m -skipbindings -tags prod 13 | 14 | prod-noupdate: 15 | desc: Create a production build of the application with self-updates disabled. Doesn't build an installer. 16 | cmds: 17 | - wails build -o Zen.exe -platform "windows/{{default .ARCH64 .ARCH}}" -ldflags "-X 'github.com/ZenPrivacy/zen-desktop/internal/cfg.Version={{.GIT_TAG}}' -X 'github.com/ZenPrivacy/zen-desktop/internal/selfupdate.NoSelfUpdate=true'" -m -skipbindings -tags prod 18 | -------------------------------------------------------------------------------- /internal/sysproxy/exclusions/README.md: -------------------------------------------------------------------------------- 1 | # https-exclusions 2 | 3 | This repository contains hostnames that are excluded from proxying by Zen Personal and Enterprise. Hostnames may be excluded for: 4 | - __Security__: sensitive personal data, financial infrastructure, government websites, and more 5 | - __Compatibility__: hostnames that break when accessed through a proxy due to MITM issues or false-positive bot detection 6 | 7 | The lists are: 8 | - [`common.txt`](/common.txt) – excluded on all platforms 9 | - [`windows.txt`](/windows.txt) – excluded on Windows 10 | - [`darwin.txt`](/darwin.txt) – excluded on macOS 11 | 12 | ## Contributing 13 | 14 | Contributions are welcome! To suggest a hostname for exclusion, please open an issue or submit a pull request with the hostname and a brief explanation of why it should be excluded. 15 | 16 | ## License 17 | 18 | This repository is licensed under the [MIT License](/LICENSE). 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: ZenPrivacy 5 | open_collective: zen-privacy 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | # polar: # Replace with a single Polar username 13 | # buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | # thanks_dev: # Replace with a single thanks.dev username 15 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /docs/internal/filter-lists.md: -------------------------------------------------------------------------------- 1 | # Filter Lists 2 | 3 | ## Which lists get a "trusted" status 4 | 5 | > [Originally discussed here](https://github.com/ZenPrivacy/zen-desktop/issues/147#issuecomment-2521317897) 6 | 7 | 1. This problem should be approached in a manner similar to the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege). This means that **only lists that use trusted scriptlets (and use scriptlets at all) should be granted a trusted status**. 8 | 2. We should **keep the number of trusted filter lists to a minimum**. I suggest setting a limit of 5 for now. 9 | 3. Trusted filter lists should either **be open-source and distributed via a repo-linked CDN (such as GitHub), or maintained by a trusted, community driven organization**. 10 | 11 | Considering the lists currently included in our default configuration, I propose granting trusted status to the following two lists: 12 | 1. AdGuard Base filter 13 | 2. AdGuard Spyware filter 14 | -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | @import 'inter.css'; 2 | @import 'normalize.css'; 3 | @import '@blueprintjs/core/lib/css/blueprint.css'; 4 | @import '@blueprintjs/icons/lib/css/blueprint-icons.css'; 5 | 6 | @font-face { 7 | font-family: 'Twemoji Country Flags'; 8 | unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, 9 | U+E007F; 10 | src: url('./assets/fonts/TwemojiCountryFlags.woff2') format('woff2'); 11 | font-display: swap; 12 | } 13 | 14 | html, 15 | body { 16 | height: 100%; 17 | width: 100%; 18 | overflow: hidden; 19 | font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif; 20 | font-feature-settings: 21 | 'liga' 1, 22 | 'calt' 1; 23 | font-variation-settings: 'opsz' 24; 24 | } 25 | 26 | @supports (font-variation-settings: normal) { 27 | html, 28 | body { 29 | font-family: InterVariable, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | modules-download-mode: readonly 5 | timeout: 5m 6 | 7 | linters: 8 | enable: 9 | - asciicheck 10 | - bidichk 11 | - gocritic 12 | - godot 13 | - gosec 14 | - importas 15 | - makezero 16 | - misspell 17 | - prealloc 18 | - predeclared 19 | - revive 20 | - staticcheck 21 | - tparallel 22 | - unconvert 23 | - unparam 24 | disable: 25 | - errcheck 26 | settings: 27 | gosec: 28 | config: 29 | G302: "0644" 30 | G306: "0644" 31 | exclusions: 32 | generated: lax 33 | presets: 34 | - comments 35 | - common-false-positives 36 | - legacy 37 | - std-error-handling 38 | rules: 39 | - linters: 40 | # These give false positives for Windows API-related identifier names. 41 | - revive 42 | - staticcheck 43 | path: (.+)_windows.go 44 | 45 | formatters: 46 | enable: 47 | - gofmt 48 | exclusions: 49 | generated: lax 50 | -------------------------------------------------------------------------------- /docs/external/how-to-rules.md: -------------------------------------------------------------------------------- 1 | # How to write rules 2 | 3 | ## FAQ 4 | 5 | ### How do I whitelist (allow) requests to a domain? 6 | 7 | To whitelist requests to a domain, use the `@@` prefix. 8 | 9 | For example, to whitelist Firefox's telemetry, use: 10 | 11 | ```plaintext 12 | @@||incoming.telemetry.mozilla.org 13 | ``` 14 | 15 | > [!IMPORTANT] 16 | > Whitelisting does **not** prevent requests from being *proxied* by Zen. To exclude domains from proxying, use the **Ignored Hosts** feature in Settings. 17 | 18 | ## See also 19 | 20 | Zen provides partial compatibility with popular filter list formats (EasyList, uBlock Origin, AdGuard, etc.), with near-complete compatibility targeted for the v1.0 release. Full documentation of supported features is a work in progress, but you can refer to the following resources for more information: 21 | 22 | - [AdGuard – How to create your own ad filters](https://adguard.com/kb/general/ad-filtering/create-own-filters) 23 | - [Adblock Plus – Filter cheatsheet](https://adblockplus.org/filter-cheatsheet) 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | 10 | 11 | ### How did you verify your code works? 12 | 13 | 19 | 20 | ### What are the relevant issues? 21 | 22 | 27 | -------------------------------------------------------------------------------- /frontend/src/FilterLists/ExportFilterList.tsx: -------------------------------------------------------------------------------- 1 | import { MenuItem } from '@blueprintjs/core'; 2 | import { useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { ExportCustomFilterLists } from '../../wailsjs/go/app/App'; 6 | import { AppToaster } from '../common/toaster'; 7 | 8 | export function ExportFilterList() { 9 | const { t } = useTranslation(); 10 | const [loading, setLoading] = useState(false); 11 | 12 | const handleExport = async () => { 13 | setLoading(true); 14 | try { 15 | await ExportCustomFilterLists(); 16 | AppToaster.show({ 17 | message: t('exportFilterList.successMessage'), 18 | intent: 'success', 19 | }); 20 | } catch (error) { 21 | AppToaster.show({ 22 | message: t('exportFilterList.errorMessage', { error }), 23 | intent: 'danger', 24 | }); 25 | } finally { 26 | setLoading(false); 27 | } 28 | }; 29 | 30 | return ; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/Intro/WelcomeScreen/LocaleList/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Radio } from '@blueprintjs/core'; 2 | 3 | import { LOCALE_LABELS, SupportedLocale } from '../../../i18n'; 4 | 5 | import './index.css'; 6 | 7 | interface LocaleListProps { 8 | selectedLocale: SupportedLocale; 9 | onSelect: (locale: SupportedLocale) => void; 10 | } 11 | 12 | export function LocaleList({ selectedLocale, onSelect }: LocaleListProps) { 13 | return ( 14 |
15 | {LOCALE_LABELS.map((locale) => ( 16 | onSelect(locale.value)} 22 | > 23 |
24 | 25 |
26 |
27 | ))} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/SettingsManager/ThemeSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import { Radio, RadioGroup, FormGroup } from '@blueprintjs/core'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { ThemeType, useTheme } from '../../common/ThemeManager'; 5 | 6 | export function ThemeSelector() { 7 | const { t } = useTranslation(); 8 | const { theme, setTheme } = useTheme(); 9 | 10 | return ( 11 | 12 | ) => { 15 | const value = e.currentTarget.value as ThemeType; 16 | setTheme(value); 17 | }} 18 | selectedValue={theme} 19 | className="theme-selector__radio-group" 20 | > 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/release-manifest.yml: -------------------------------------------------------------------------------- 1 | name: Upload release manifest 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | upload-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Node 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version-file: scripts/package.json 19 | cache: 'npm' 20 | cache-dependency-path: scripts/package-lock.json 21 | - name: Run node --version 22 | run: node --version 23 | - name: Install dependencies 24 | run: npm ci --omit=dev 25 | working-directory: scripts 26 | - name: Run upload-manifest 27 | env: 28 | S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }} 29 | S3_API_ENDPOINT: ${{ secrets.S3_API_ENDPOINT }} 30 | S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} 31 | S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} 32 | run: npm run upload-manifest 33 | working-directory: scripts 34 | -------------------------------------------------------------------------------- /internal/cfg/systemdirs_linux.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 8 | ) 9 | 10 | const ( 11 | appFolderName = constants.AppNameLowercase 12 | ) 13 | 14 | // On Linux, we use the XDG Base Directory Specification: 15 | // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables 16 | 17 | func getConfigDir() (string, error) { 18 | if os.Getenv("XDG_CONFIG_HOME") != "" { 19 | return filepath.Join(os.Getenv("XDG_CONFIG_HOME"), appFolderName), nil 20 | } 21 | 22 | homeDir, err := os.UserHomeDir() 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | return filepath.Join(homeDir, ".config", appFolderName), nil 28 | } 29 | 30 | func getDataDir() (string, error) { 31 | if os.Getenv("XDG_DATA_HOME") != "" { 32 | return filepath.Join(os.Getenv("XDG_DATA_HOME"), appFolderName), nil 33 | } 34 | 35 | homeDir, err := os.UserHomeDir() 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | return filepath.Join(homeDir, ".local", "share", appFolderName), nil 41 | } 42 | -------------------------------------------------------------------------------- /tasks/build/Taskfile-linux.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | vars: 4 | ARCH64: '{{if eq ARCH "arm"}}arm64{{else}}{{ARCH}}{{end}}' 5 | GIT_TAG: 6 | sh: git describe --tags --always --abbrev=0 7 | 8 | tasks: 9 | prod: 10 | desc: Create a production build of the application. 11 | cmds: 12 | - wails build -o Zen -platform "linux/{{default .ARCH64 .ARCH}}" -ldflags "-X 'github.com/ZenPrivacy/zen-desktop/internal/cfg.Version={{.GIT_TAG}}'" -m -skipbindings -tags prod,webkit2_41 13 | 14 | prod-noupdate: 15 | desc: Create a production build of the application with self-updates disabled. 16 | cmds: 17 | - wails build -o Zen -platform "linux/{{default .ARCH64 .ARCH}}" -ldflags "-X 'github.com/ZenPrivacy/zen-desktop/internal/cfg.Version={{.GIT_TAG}}' -X 'github.com/ZenPrivacy/zen-desktop/internal/selfupdate.NoSelfUpdate=true'" -m -skipbindings -tags prod,webkit2_41 18 | 19 | deps: 20 | desc: Install the apt dependencies required to create a production build. 21 | cmds: 22 | - sudo apt update && sudo apt install libgtk-3-0 gcc-aarch64-linux-gnu libwebkit2gtk-4.1-dev 23 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier'], 5 | extends: ['airbnb', 'airbnb-typescript', 'plugin:import/typescript', 'plugin:prettier/recommended'], 6 | rules: { 7 | 'react/react-in-jsx-scope': 0, 8 | 'import/order': [ 9 | 'error', 10 | { 11 | 'newlines-between': 'always', 12 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 13 | alphabetize: { 14 | order: 'asc', 15 | caseInsensitive: true, 16 | }, 17 | }, 18 | ], 19 | 'import/prefer-default-export': 0, 20 | '@typescript-eslint/no-use-before-define': 0, 21 | '@typescript-eslint/no-shadow': 0, 22 | 'react/no-unstable-nested-components': 0, 23 | 'react/require-default-props': 0, 24 | 'import/no-relative-packages': 0, 25 | 'no-console': 0, 26 | }, 27 | parserOptions: { 28 | project: './tsconfig.json', 29 | tsconfigRootDir: __dirname, 30 | }, 31 | ignorePatterns: ['node_modules/', 'wailsjs/', 'vite.config.ts'], 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/FilterLists/ImportFilterList.tsx: -------------------------------------------------------------------------------- 1 | import { MenuItem } from '@blueprintjs/core'; 2 | import { useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { ImportCustomFilterLists } from '../../wailsjs/go/app/App'; 6 | import { AppToaster } from '../common/toaster'; 7 | 8 | export function ImportFilterList({ onAdd }: { onAdd: () => void }) { 9 | const { t } = useTranslation(); 10 | const [loading, setLoading] = useState(false); 11 | 12 | const handleImport = async () => { 13 | setLoading(true); 14 | try { 15 | await ImportCustomFilterLists(); 16 | AppToaster.show({ 17 | message: t('importFilterList.successMessage'), 18 | intent: 'success', 19 | }); 20 | onAdd(); 21 | } catch (error) { 22 | AppToaster.show({ 23 | message: t('importFilterList.errorMessage', { error }), 24 | intent: 'danger', 25 | }); 26 | } finally { 27 | setLoading(false); 28 | } 29 | }; 30 | 31 | return ; 32 | } 33 | -------------------------------------------------------------------------------- /internal/cfg/systemdirs_windows.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 8 | ) 9 | 10 | const ( 11 | appFolderName = constants.AppName 12 | configDirName = "Config" 13 | ) 14 | 15 | func getConfigDir() (string, error) { 16 | // use LOCALAPPDATA instead of APPDATA because the config includes some machine-specific data 17 | // e.g. whether the CA has been installed 18 | if os.Getenv("LOCALAPPDATA") != "" { 19 | return filepath.Join(os.Getenv("LOCALAPPDATA"), appFolderName, configDirName), nil 20 | } 21 | 22 | homeDir, err := os.UserHomeDir() 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | return filepath.Join(homeDir, "AppData", "Local", appFolderName, configDirName), nil 28 | } 29 | 30 | func getDataDir() (string, error) { 31 | if os.Getenv("LOCALAPPDATA") != "" { 32 | return filepath.Join(os.Getenv("LOCALAPPDATA"), appFolderName), nil 33 | } 34 | 35 | homeDir, err := os.UserHomeDir() 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | return filepath.Join(homeDir, "AppData", "Local", appFolderName), nil 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/SettingsManager/index.css: -------------------------------------------------------------------------------- 1 | .settings-manager { 2 | margin: 0.25rem 0 0.5rem 0; 3 | } 4 | 5 | .settings-manager__section-header { 6 | border-radius: 2px 2px 0 0; 7 | font-weight: bold; 8 | } 9 | 10 | .settings-manager__section--app { 11 | border: 1px solid #245ac7; 12 | border-radius: 4px; 13 | margin-bottom: 8px; 14 | } 15 | .settings-manager__section--links{ 16 | display: flex; 17 | gap: 8px; 18 | margin-bottom:8px; 19 | } 20 | .settings-manager__section--advanced { 21 | border: 1px solid #fbb360; 22 | border-radius: 4px; 23 | margin-bottom: 4px; 24 | } 25 | 26 | .settings-manager__ignored-hosts-input { 27 | width: 100% !important; 28 | max-height: 40vh; 29 | } 30 | 31 | .settings-manager__ignored-hosts-tooltip { 32 | width: 100%; 33 | } 34 | 35 | .settings-manager__section-body { 36 | padding: 10px 10px 0 10px; 37 | } 38 | 39 | .settings-manager__about { 40 | margin-top: 16px; 41 | text-align: center; 42 | font-size: 0.8rem; 43 | } 44 | 45 | .settings-manager__about-changelog::before { 46 | content: ' '; 47 | } 48 | 49 | .settings-manager__about-github-button { 50 | font-size: 0.8rem; 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read 14 | checks: write 15 | 16 | jobs: 17 | test-go: 18 | name: Test Go (${{ matrix.os }}) 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | fail-fast: false 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version-file: ./go.mod 30 | - name: Run go version 31 | run: go version 32 | - name: Set up Task 33 | uses: arduino/setup-task@v1 34 | with: 35 | version: 3.x 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | - name: Create placeholder asset 38 | # go:embed requires the directory to exist and be non-empty 39 | run: | 40 | mkdir -p ./frontend/dist 41 | touch ./frontend/dist/placeholder 42 | - name: Run tests 43 | run: task test 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zen Privacy Project Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/Intro/FilterListsScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { Trans, useTranslation } from 'react-i18next'; 2 | 3 | import { filter } from '../../../wailsjs/go/models'; 4 | import { FilterListItem } from '../../FilterLists'; 5 | 6 | interface FilterListsScreenProps { 7 | filterLists: filter.List[]; 8 | } 9 | 10 | export function FilterListsScreen({ filterLists }: FilterListsScreenProps) { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 |
15 |

{t('intro.filterLists.title')}

16 |

17 | , 21 | }} 22 | /> 23 |

24 |

{t('intro.filterLists.recommendation')}

25 |
26 | {filterLists.map((l) => ( 27 | 28 | ))} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /internal/logger/README.md: -------------------------------------------------------------------------------- 1 | # `logger` 2 | 3 | ## Why We Need Different Setups for Production and Non-Production Builds on Windows 4 | 5 | When running `wails build`, Wails builds the app with the [`-H windowsgui` linker flag](https://pkg.go.dev/cmd/link). This sets the [\SUBSYSTEM PE header variable](https://learn.microsoft.com/en-us/cpp/build/reference/subsystem-specify-subsystem?view=msvc-170&redirectedfrom=MSDN) to `WINDOWS`. Processes configured with this value have their standard I/O handles [closed by default](https://learn.microsoft.com/en-us/windows/console/getstdhandle?redirectedfrom=MSDN#remarks). However, during `wails dev`, the I/O handlers function as expected. 6 | 7 | In development mode, we use `io.MultiWriter` to write logs to both `os.Stdout` and a file logger. The `io.MultiWriter` function [synchronously writes to each of its children](https://go.dev/src/io/multi.go#L83), meaning that if any single child hangs, the entire operation hangs. This becomes problematic in production because the closed `os.Stdout` handle will cause each log operation to hang indefinitely. 8 | 9 | To avoid this issue, we log **exclusively to the filesystem in production**. 10 | -------------------------------------------------------------------------------- /internal/cfg/systemdirs_darwin.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 8 | ) 9 | 10 | const ( 11 | appFolderName = constants.AppName 12 | configDirName = "Config" 13 | ) 14 | 15 | func getConfigDir() (string, error) { 16 | // According to Apple's guidelines, files in the ~/Library/Preferences should be only managed using native APIs, 17 | // so we use a subfolder in ~/Library/Application Support instead. 18 | // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1 19 | 20 | homeDir, err := os.UserHomeDir() 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | dir := filepath.Join(homeDir, "Library", "Application Support", appFolderName, configDirName) 26 | return dir, nil 27 | } 28 | 29 | func getDataDir() (string, error) { 30 | homeDir, err := os.UserHomeDir() 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | dir := filepath.Join(homeDir, "Library", "Application Support", appFolderName) 36 | return dir, nil 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/SettingsManager/ExportLogsButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from '@blueprintjs/core'; 2 | import { useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { OpenLogsDirectory } from '../../../wailsjs/go/app/App'; 6 | import { AppToaster } from '../../common/toaster'; 7 | 8 | export function ExportLogsButton() { 9 | const [loading, setLoading] = useState(false); 10 | const { t } = useTranslation(); 11 | 12 | return ( 13 | 14 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /internal/sysproxy/exclusions/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zen Privacy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/internal/project-structure.md: -------------------------------------------------------------------------------- 1 | # Project structure 2 | 3 | This document describes the file structure of the project. 4 | 5 | - `.github`: GitHub Actions workflows and issue templates. 6 | - `assets`: Assets used in README.md and other documents (not in source files). 7 | - `build`: Build configuration and artifacts. Read more in [build/README.md](../../build/README.md). 8 | - `docs/internal`: Internal documentation for contributors. 9 | - `docs/external`: External documentation for users. 10 | - `frontend`: Frontend JS/TS code. Read more in [frontend/README.md](../../frontend/README.md). 11 | - `internal`: Backend Go packages. 12 | - `scriptlets`: JS/TS functions injected into webpages for advanced content blocking. 13 | - `scripts`: Node.js scripts for manifest file uploads. May be used for other purposes in the future. 14 | - `tasks`: Platform-specific build-related [Taskfiles](https://taskfile.dev). 15 | - `main.go`: The main entry point of the application. 16 | - `golangci.yml`: Configuration file for [golangci-lint](https://golangci-lint.run). 17 | - `Taskfile.yml`: Main [Taskfile](https://taskfile.dev) for common development tasks. 18 | - `wails.json`: [Wails](https://wails.io) configuration file. 19 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/app/App.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export function ExportCustomFilterLists() { 6 | return window['go']['app']['App']['ExportCustomFilterLists'](); 7 | } 8 | 9 | export function ImportCustomFilterLists() { 10 | return window['go']['app']['App']['ImportCustomFilterLists'](); 11 | } 12 | 13 | export function IsNoSelfUpdate() { 14 | return window['go']['app']['App']['IsNoSelfUpdate'](); 15 | } 16 | 17 | export function OnSecondInstanceLaunch(arg1) { 18 | return window['go']['app']['App']['OnSecondInstanceLaunch'](arg1); 19 | } 20 | 21 | export function OpenLogsDirectory() { 22 | return window['go']['app']['App']['OpenLogsDirectory'](); 23 | } 24 | 25 | export function RestartApplication() { 26 | return window['go']['app']['App']['RestartApplication'](); 27 | } 28 | 29 | export function StartProxy() { 30 | return window['go']['app']['App']['StartProxy'](); 31 | } 32 | 33 | export function StopProxy() { 34 | return window['go']['app']['App']['StopProxy'](); 35 | } 36 | 37 | export function UninstallCA() { 38 | return window['go']['app']['App']['UninstallCA'](); 39 | } 40 | -------------------------------------------------------------------------------- /docs/internal/testing-checklist.md: -------------------------------------------------------------------------------- 1 | # Manual Checklist 2 | 3 | ## Home 4 | 5 | - Start proxy 6 | - Start proxy without CA installed 7 | - Stop proxy 8 | 9 | ## Filter lists 10 | 11 | - Filter lists counters are correct (enabled/total) 12 | - Add custom filter list 13 | - Add custom filter list with invalid URL (error) 14 | - Add custom filter list without name (name becomes same as URL) 15 | - Add trusted custom filter list 16 | - Add filter list with duplicate URL (error) 17 | - Delete custom filter list 18 | - Import filter list 19 | - Import filter list in incorrect format 20 | - Export filter lists 21 | - Enable filter list 22 | - Disable filter list 23 | 24 | ## My rules 25 | 26 | - Add My rule (e.g. `0.0.0.0 www.google.com`) 27 | - Remove My rule 28 | - "Help" button opens guide on How to write rules 29 | 30 | ## Preferences 31 | 32 | - Change language 33 | - Enable "Autostart" (Windows/macOS/Linux) 34 | - Disable "Autostart" (Windows/macOS/Linux) 35 | - Export logs 36 | - Set fixed Port 37 | - Add ignored hosts 38 | - Remove ignored hosts 39 | - Uninstall CA 40 | - Uninstall CA while proxy is running 41 | 42 | ## Self‑update (Windows/macOS/Linux) 43 | 44 | - Disable updates 45 | - Select "Ask before updating" 46 | - Select "Automatic updates" 47 | -------------------------------------------------------------------------------- /frontend/src/context/ProxyStateContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, ReactNode, useState, useMemo } from 'react'; 2 | 3 | import { ProxyState } from '../types'; 4 | 5 | type ProxyStateContextType = { 6 | proxyState: ProxyState; 7 | setProxyState: (state: ProxyState) => void; 8 | isProxyRunning: boolean; 9 | }; 10 | 11 | const ProxyStateContext = createContext(undefined); 12 | 13 | export function ProxyStateProvider({ children }: { children: ReactNode }) { 14 | const [proxyState, setProxyState] = useState('off'); 15 | const isProxyRunning = proxyState === 'on' || proxyState === 'loading'; 16 | 17 | // Memoize the context value to prevent unnecessary re-renders 18 | const contextValue = useMemo( 19 | () => ({ 20 | proxyState, 21 | setProxyState, 22 | isProxyRunning, 23 | }), 24 | [proxyState, isProxyRunning], 25 | ); 26 | 27 | return {children}; 28 | } 29 | 30 | export function useProxyState() { 31 | const context = useContext(ProxyStateContext); 32 | if (context === undefined) { 33 | throw new Error('useProxyState must be used within a ProxyStateProvider'); 34 | } 35 | return context; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "engines": { 5 | "node": ">=20.11.1" 6 | }, 7 | "type": "module", 8 | "scripts": { 9 | "dev": "vite", 10 | "build": "tsc && vite build", 11 | "preview": "vite preview", 12 | "lint": "eslint . --ext .ts,.tsx", 13 | "extract-translations": "npx i18next-parser" 14 | }, 15 | "dependencies": { 16 | "@blueprintjs/core": "5.19.0", 17 | "@blueprintjs/icons": "5.22.0", 18 | "@blueprintjs/select": "5.3.20", 19 | "i18next": "^22.5.1", 20 | "is-emoji-supported": "^0.0.5", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-i18next": "^12.3.1", 24 | "use-debounce": "^10.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/react": "^18.0.17", 28 | "@types/react-dom": "^18.0.6", 29 | "@typescript-eslint/eslint-plugin": "^6.6.0", 30 | "@typescript-eslint/parser": "^6.6.0", 31 | "@vitejs/plugin-react": "^2.2.0", 32 | "eslint": "^8.48.0", 33 | "eslint-config-airbnb": "^19.0.4", 34 | "eslint-config-airbnb-typescript": "^17.1.0", 35 | "eslint-config-prettier": "^9.0.0", 36 | "eslint-plugin-prettier": "^5.0.1", 37 | "i18next-parser": "^9.3.0", 38 | "prettier": "^3.0.3", 39 | "typescript": "^4.6.4", 40 | "vite": "^3.2.11" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/external/known-issues-and-troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Known issues and troubleshooting 2 | 3 | ## Firefox 4 | 5 | ### MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE 6 | 7 | It's possible when visiting sites, that the page will fail to render and you will see 8 | 9 | "Secure Connection Failed" with the Error code: `MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE` 10 | 11 | A work around to this is setting `security.cert_pinning.enforcement_level` in `about:config` to 1 12 | 13 | ![alt text](firefox.png) 14 | 15 | If you're using arkenfox you can add the above to your `user-overrides.js` like so 16 | 17 | ```js 18 | user_pref("security.cert_pinning.enforcement_level", 1); 19 | ``` 20 | 21 | ### MOZILLA_PKIX_ERROR_MITM_DETECTED 22 | 23 | Another issue that may pop up is `MOZILLA_PKIX_ERROR_MITM_DETECTED` 24 | 25 | This can be fixed in a few ways: 26 | 27 | Sometimes a simple restart of the browser is all that's needed, if that doesn't work you can go to 28 | 29 | `Settings > Privacy & Security` and scroll down to `Certificates` 30 | 31 | Once there, tick the checkbox for "Allow Firefox to automatically trust third-party root certificates you install" 32 | 33 | And finally you can set `security.enterprise_roots.enabled` to `true` in `about:config` and restart Firefox. This appears to have the same effect as the above fix, according to the Firefox docs. However, it's good to check both just incase. 34 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 11 | ) 12 | 13 | func OpenLogsDirectory() error { 14 | logsDir, err := getLogsDir(constants.AppName) 15 | if err != nil { 16 | return fmt.Errorf("get logs directory: %w", err) 17 | } 18 | 19 | switch runtime.GOOS { 20 | case "windows": 21 | return exec.Command("explorer", logsDir).Start() 22 | case "darwin": 23 | return exec.Command("open", logsDir).Start() 24 | case "linux": 25 | return exec.Command("xdg-open", logsDir).Start() 26 | default: 27 | panic("unsupported platform") 28 | } 29 | } 30 | 31 | func getLogsDir(appName string) (string, error) { 32 | var path string 33 | homeDir, err := os.UserHomeDir() 34 | if err != nil { 35 | return "", fmt.Errorf("get user home directory: %w", err) 36 | } 37 | 38 | switch runtime.GOOS { 39 | case "windows": 40 | path = filepath.Join(os.Getenv("LOCALAPPDATA"), appName, "Logs") 41 | case "darwin": 42 | path = filepath.Join(homeDir, "Library", "Logs", appName) 43 | case "linux": 44 | path = filepath.Join(homeDir, ".local", "share", appName, "logs") 45 | } 46 | 47 | if err := os.MkdirAll(path, 0755); err != nil { 48 | return "", fmt.Errorf("create log directory: %v", err) 49 | } 50 | 51 | return path, nil 52 | } 53 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | build: tasks/build/Taskfile-{{OS}}.yml 5 | frontend: frontend/Taskfile.yml 6 | 7 | tasks: 8 | default: 9 | desc: Runs the dev task. 10 | cmds: 11 | - task: dev 12 | 13 | dev: 14 | desc: Runs the application in development mode. 15 | cmds: 16 | - wails dev 17 | 18 | build-dev: 19 | desc: Create a development build of the application. 20 | cmds: 21 | - wails build 22 | 23 | test-go: 24 | desc: Run Go tests. 25 | cmds: 26 | - go test -cover -race ./... 27 | 28 | test: 29 | desc: Run Go tests. 30 | cmds: 31 | - task: test-go 32 | - cmd: echo "Tests passed" 33 | silent: true 34 | 35 | lint: 36 | desc: Run frontend and Go linters. 37 | cmds: 38 | - task: frontend:lint 39 | - task: lint-go 40 | - cmd: echo "Checks passed" 41 | silent: true 42 | 43 | lint-go: 44 | desc: Run Go linters. 45 | cmds: 46 | - golangci-lint run 47 | 48 | fmt-go: 49 | desc: Run go fmt on improperly formatted Go files. 50 | cmds: 51 | - golangci-lint fmt 52 | 53 | pull-exclusions: 54 | desc: Pull the updates from the zen-https-exclusions repository. 55 | cmds: 56 | - git subtree pull --prefix=internal/sysproxy/exclusions https://github.com/ZenPrivacy/zen-https-exclusions master --squash 57 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/AppHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { DonateButton } from '../../DonateButton'; 2 | import './index.css'; 3 | 4 | export function AppHeader() { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/Intro/SettingsScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { Callout, Card, Divider } from '@blueprintjs/core'; 2 | import { useEffect, useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { IsNoSelfUpdate } from '../../../wailsjs/go/app/App'; 6 | import { AutostartSwitch } from '../../SettingsManager/AutostartSwitch'; 7 | import { AutoupdateSwitch } from '../../SettingsManager/AutoupdateSwitch'; 8 | 9 | import './index.css'; 10 | 11 | export function SettingsScreen() { 12 | const { t } = useTranslation(); 13 | const [showUpdatePolicy, setShowUpdatePolicy] = useState(false); 14 | 15 | useEffect(() => { 16 | IsNoSelfUpdate().then((noSelfUpdate) => { 17 | setShowUpdatePolicy(!noSelfUpdate); 18 | }); 19 | }, []); 20 | 21 | return ( 22 |
23 |

{t('intro.settings.title')}

24 |

{t('intro.settings.description')}

25 | 26 | 27 | 28 | 29 | {showUpdatePolicy && ( 30 | <> 31 | 32 | 33 | 34 | )} 35 | 36 | 37 | 38 | {t('intro.settings.settingsNote')} 39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/models.ts: -------------------------------------------------------------------------------- 1 | export namespace cfg { 2 | 3 | export enum UpdatePolicyType { 4 | AUTOMATIC = "automatic", 5 | PROMPT = "prompt", 6 | DISABLED = "disabled", 7 | } 8 | 9 | } 10 | 11 | export namespace filter { 12 | 13 | export class List { 14 | name: string; 15 | type: string; 16 | url: string; 17 | enabled: boolean; 18 | trusted: boolean; 19 | locales: string[]; 20 | 21 | static createFrom(source: any = {}) { 22 | return new List(source); 23 | } 24 | 25 | constructor(source: any = {}) { 26 | if ('string' === typeof source) source = JSON.parse(source); 27 | this.name = source["name"]; 28 | this.type = source["type"]; 29 | this.url = source["url"]; 30 | this.enabled = source["enabled"]; 31 | this.trusted = source["trusted"]; 32 | this.locales = source["locales"]; 33 | } 34 | } 35 | 36 | } 37 | 38 | export namespace options { 39 | 40 | export class SecondInstanceData { 41 | Args: string[]; 42 | WorkingDirectory: string; 43 | 44 | static createFrom(source: any = {}) { 45 | return new SecondInstanceData(source); 46 | } 47 | 48 | constructor(source: any = {}) { 49 | if ('string' === typeof source) source = JSON.parse(source); 50 | this.Args = source["Args"]; 51 | this.WorkingDirectory = source["WorkingDirectory"]; 52 | } 53 | } 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /frontend/src/SettingsManager/ExportDebugDataButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from '@blueprintjs/core'; 2 | import { useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { ExportDebugData } from '../../../wailsjs/go/cfg/Config'; 6 | import { ClipboardSetText } from '../../../wailsjs/runtime'; 7 | import { AppToaster } from '../../common/toaster'; 8 | 9 | export function ExportDebugDataButton() { 10 | const [loading, setLoading] = useState(false); 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 | 15 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/FilterLists/index.css: -------------------------------------------------------------------------------- 1 | .filter-lists__spinner { 2 | margin-top: 8px; 3 | } 4 | 5 | .filter-lists__list { 6 | border-bottom: 1px dotted #333; 7 | padding-top: 10px; 8 | padding-bottom: 10px; 9 | } 10 | 11 | .bp5-dark .filter-lists__list { 12 | border-bottom: 1px dotted #f5f5f5; 13 | } 14 | 15 | .filter-lists__list-header { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin-bottom: 6px; 20 | } 21 | 22 | .filter-lists__list-trusted { 23 | margin-bottom: 6px; 24 | } 25 | 26 | .filter-lists__select-count { 27 | display: inline-block; 28 | margin-left: 5px; 29 | font-variant-numeric: tabular-nums; 30 | } 31 | 32 | .filter-lists__list-switch { 33 | margin: 0; 34 | } 35 | 36 | .filter-lists__list-name { 37 | margin: 0; 38 | -webkit-line-clamp: 1; 39 | overflow: hidden; 40 | text-overflow: ellipsis; 41 | white-space: nowrap; 42 | } 43 | 44 | .filter-lists__list-url { 45 | -webkit-line-clamp: 1; 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | white-space: nowrap; 49 | } 50 | 51 | .filter-lists__create-filter-list { 52 | border-bottom: 1px dotted #fff; 53 | padding-top: 10px; 54 | padding-bottom: 10px; 55 | } 56 | 57 | .filter-lists__list-delete { 58 | margin-top: 10px; 59 | } 60 | 61 | .filter-lists__header { 62 | display: flex; 63 | justify-content: space-between; 64 | } 65 | 66 | .filter-lists__list-buttons { 67 | display: flex; 68 | margin-top: 10px; 69 | gap: 10px; 70 | } 71 | 72 | .filter-lists__list-button { 73 | width: 100%; 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/Intro/WelcomeScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { changeLocale, getCurrentLocale } from '../../i18n'; 5 | 6 | import { LocaleList } from './LocaleList'; 7 | 8 | import './index.css'; 9 | 10 | const getTranslationsFor = (languageCode: string) => { 11 | const tfixed = i18next.getFixedT(languageCode); 12 | return { 13 | welcome: tfixed('intro.welcome.title'), 14 | description: tfixed('intro.welcome.description'), 15 | }; 16 | }; 17 | 18 | export function WelcomeScreen() { 19 | const [locale, setLocale] = useState(getCurrentLocale); 20 | const [welcomeText, setWelcomeText] = useState(''); 21 | const [descriptionText, setDescriptionText] = useState(''); 22 | 23 | useEffect(() => { 24 | if (!locale) return; 25 | 26 | const texts = getTranslationsFor(locale); 27 | setWelcomeText(texts.welcome); 28 | setDescriptionText(texts.description); 29 | }, [locale]); 30 | 31 | return ( 32 |
33 |
34 |

35 | 👋 {welcomeText} 36 |

37 |

38 | {descriptionText} 39 |

40 |
41 | { 43 | setLocale(locale); 44 | changeLocale(locale); 45 | }} 46 | selectedLocale={locale} 47 | /> 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { isEmojiSupported } from 'is-emoji-supported'; 2 | import React from 'react'; 3 | import { createRoot } from 'react-dom/client'; 4 | 5 | import App from './App'; 6 | import { ThemeProvider } from './common/ThemeManager'; 7 | import { ProxyStateProvider } from './context/ProxyStateContext'; 8 | import ErrorBoundary from './ErrorBoundary'; 9 | import { initI18n } from './i18n'; 10 | import './style.css'; 11 | 12 | (function polyfillCountryFlagEmojis() { 13 | if (!isEmojiSupported('😊') || isEmojiSupported('🇨🇭')) { 14 | return; 15 | } 16 | 17 | const style = document.createElement('style'); 18 | style.innerHTML = ` 19 | html, body { 20 | font-family: 'Twemoji Country Flags', Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif; 21 | } 22 | 23 | @supports (font-variation-settings: normal) { 24 | html, body { 25 | font-family: 'Twemoji Country Flags', InterVariable, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif; 26 | } 27 | } 28 | `; 29 | document.head.appendChild(style); 30 | })(); 31 | 32 | async function bootstrap() { 33 | await initI18n(); 34 | 35 | const container = document.getElementById('root'); 36 | const root = createRoot(container!); 37 | 38 | root.render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | , 48 | ); 49 | } 50 | 51 | bootstrap(); 52 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /internal/selfupdate/executable.go: -------------------------------------------------------------------------------- 1 | package selfupdate 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/ZenPrivacy/zen-desktop/internal/constants" 11 | ) 12 | 13 | func replaceExecutable(tempDir string) error { 14 | expectedExecName := constants.AppName 15 | if runtime.GOOS == "windows" { 16 | expectedExecName += ".exe" 17 | } 18 | newExecPath := filepath.Join(tempDir, expectedExecName) 19 | 20 | if _, err := os.Stat(newExecPath); os.IsNotExist(err) { 21 | return fmt.Errorf("expected executable '%s' not found", expectedExecName) 22 | } 23 | 24 | currentExecPath, err := getExecPath() 25 | if err != nil { 26 | return fmt.Errorf("get exec path: %w", err) 27 | } 28 | 29 | if err := os.Rename(newExecPath, currentExecPath); err != nil { 30 | return fmt.Errorf("move new executable: %w", err) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func getExecPath() (string, error) { 37 | execPath, err := os.Executable() 38 | if err != nil { 39 | return "", fmt.Errorf("get executable path: %w", err) 40 | } 41 | 42 | // https://github.com/golang/go/issues/40966 43 | if runtime.GOOS != "windows" { 44 | if execPath, err = filepath.EvalSymlinks(execPath); err != nil { 45 | return "", fmt.Errorf("eval symlinks: %w", err) 46 | } 47 | } 48 | 49 | return execPath, nil 50 | } 51 | 52 | func findAppBundlePath(execPath string) string { 53 | dir := filepath.Dir(execPath) 54 | for dir != "/" { 55 | if strings.HasSuffix(dir, ".app") { 56 | return dir 57 | } 58 | dir = filepath.Dir(dir) 59 | } 60 | return "" 61 | } 62 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the [latest version](https://github.com/ZenPrivacy/zen-desktop/releases/latest) of the app is actively supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you've discovered a security issue in Zen, please disclose it privately and responsibly by emailing us at . 10 | 11 | In your report, please include: 12 | 13 | - A description of the issue and how it could be exploited 14 | - Step-by-step instructions to reproduce the issue (if known) 15 | - The date and time you first identified the vulnerability 16 | - A list of affected platforms and versions (e.g., Windows 11, macOS 15) 17 | - Whether the vulnerability is already public or known to third parties (please provide details) 18 | - Your name and affiliation (if you'd like to be credited) 19 | - As much technical detail as possible 20 | - Any additional information that may be helpful 21 | 22 | ### Report lifecycle 23 | 24 | You should receive an initial response within 72 hours. If you don't hear back, please follow up again, or send a notice (**important: without any vulnerability details**) to a project lead via personal email or a verified social platform. 25 | 26 | After you make a report, we will work with you to confirm it and assess its impact. Once we've been able to confirm the issue, we'll work to remediate it. We ask that you keep your report confidential for 90 days after you make it, to give us a chance to remediate the issue and protect our users. 27 | 28 | After we've fixed the issue - or after 90 days, whichever comes first - we will publish a summary of the vulnerability and the actions taken. You are then free to publish your own disclosure. 29 | -------------------------------------------------------------------------------- /docs/internal/code-conventions.md: -------------------------------------------------------------------------------- 1 | # Code conventions 2 | This document outlines key code conventions followed in this project. 3 | 4 | ## Go 5 | 1. __Avoid utility packages__
6 | Do not use generic utility package names like `base`, `util`, or `common`. For more context, see [Dave Cheney's post explaining why](https://dave.cheney.net/2019/01/08/avoid-package-names-like-base-util-or-common). 7 | 2. __Error wrapping__
8 | Errors should __always__ be wrapped with `fmt.Errorf` with the `%w` directive. The wrapped message must provide enough context to help identify the error’s source at a glance. Avoid prefixes like "failed to" or "error" in messages, as they add no meaningful context. 9 | - __Bad Example__
10 | A generic message that doesn’t provide specific context: 11 | ```go 12 | if _, err := os.Create(configFileName); err != nil { 13 | return fmt.Errorf("failed to create file: %v", err) 14 | } 15 | ``` 16 | - __Good Example__
17 | A more specific message that clarifies the error’s context: 18 | ```go 19 | if _, err := os.Create(configFileName); err != nil { 20 | return fmt.Errorf("create config file: %w", err) 21 | } 22 | ``` 23 | 3. __Struct constructors__
24 | Struct constructors are specifically created for use within `app.go` and `main.go`. They may initialize dependency fields with meaningful defaults and use static variables from other packages. An implication of this is that __public tests__ should use constructors to initialize structs, and __private tests__ may create a struct manually. 25 | 26 | ### See also 27 | - [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) 28 | -------------------------------------------------------------------------------- /frontend/src/SettingsManager/PortInput.tsx: -------------------------------------------------------------------------------- 1 | import { FormGroup, NumericInput, Tooltip } from '@blueprintjs/core'; 2 | import { useEffect, useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useDebouncedCallback } from 'use-debounce'; 5 | 6 | import { GetPort, SetPort } from '../../wailsjs/go/cfg/Config'; 7 | import { useProxyState } from '../context/ProxyStateContext'; 8 | 9 | export function PortInput() { 10 | const { t } = useTranslation(); 11 | const { isProxyRunning } = useProxyState(); 12 | const [state, setState] = useState({ 13 | port: 0, 14 | loading: true, 15 | }); 16 | 17 | useEffect(() => { 18 | (async () => { 19 | const port = await GetPort(); 20 | setState({ ...state, port, loading: false }); 21 | })(); 22 | }, []); 23 | 24 | const setPort = useDebouncedCallback(async (port: number) => { 25 | await SetPort(port); 26 | }, 500); 27 | 28 | return ( 29 | 34 | {t('portInput.description')} 35 |
36 | {t('portInput.helper')} 37 | 38 | } 39 | > 40 | 41 | { 47 | setState({ ...state, port }); 48 | setPort(port); 49 | }} 50 | disabled={state.loading || isProxyRunning} 51 | /> 52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/bluesky.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/SettingsManager/LocaleSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, FormGroup, MenuItem } from '@blueprintjs/core'; 2 | import { ItemRenderer, Select } from '@blueprintjs/select'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { changeLocale, getCurrentLocale, LOCALE_LABELS, LocaleItem } from '../../i18n'; 6 | 7 | interface LocaleSelectorProps { 8 | showLabel?: boolean; 9 | showHelper?: boolean; 10 | } 11 | 12 | export function LocaleSelector({ showLabel = true, showHelper = true }: LocaleSelectorProps = {}) { 13 | const { t } = useTranslation(); 14 | 15 | const handleLocaleChange = async (item: LocaleItem) => { 16 | changeLocale(item.value); 17 | }; 18 | 19 | const renderItem: ItemRenderer = (item, { handleClick, handleFocus, modifiers }) => { 20 | return ( 21 | 29 | ); 30 | }; 31 | 32 | const currentLocale = LOCALE_LABELS.find((item) => item.value === getCurrentLocale()) || LOCALE_LABELS[0]; 33 | 34 | const selectComponent = ( 35 | 36 | items={LOCALE_LABELS} 37 | activeItem={currentLocale} 38 | onItemSelect={handleLocaleChange} 39 | itemRenderer={renderItem} 40 | filterable={false} 41 | popoverProps={{ minimal: true }} 42 | > 43 |