├── 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 | }
16 | variant="outlined"
17 | onClick={() => BrowserOpenURL(LINK)}
18 | >
19 | {t('donate')}
20 |
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 |
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 | 
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 |
11 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
45 | );
46 |
47 | if (!showLabel && !showHelper) {
48 | return selectComponent;
49 | }
50 |
51 | return (
52 |
56 | {selectComponent}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/internal/sysproxy/system_windows.go:
--------------------------------------------------------------------------------
1 | package sysproxy
2 |
3 | import (
4 | _ "embed"
5 | "fmt"
6 | "log"
7 |
8 | "golang.org/x/sys/windows"
9 | "golang.org/x/sys/windows/registry"
10 | )
11 |
12 | var (
13 | wininet = windows.NewLazySystemDLL("wininet.dll")
14 | internetSetOption = wininet.NewProc("InternetSetOptionW")
15 | internetOptionSettingsChanged = 39
16 | internetOptionRefresh = 37
17 |
18 | //go:embed exclusions/windows.txt
19 | platformSpecificExcludedHosts []byte
20 | )
21 |
22 | func setSystemProxy(pacURL string) error {
23 | k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.ALL_ACCESS)
24 | if err != nil {
25 | return err
26 | }
27 | defer k.Close()
28 |
29 | if err := k.SetDWordValue("ProxyEnable", 0); err != nil {
30 | return fmt.Errorf("set ProxyEnable: %v", err)
31 | }
32 | if err := k.SetStringValue("AutoConfigURL", pacURL); err != nil {
33 | return fmt.Errorf("set AutoConfigURL: %v", err)
34 | }
35 |
36 | callInternetSetOption(internetOptionSettingsChanged)
37 | callInternetSetOption(internetOptionRefresh)
38 |
39 | return nil
40 | }
41 |
42 | func unsetSystemProxy() error {
43 | k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.ALL_ACCESS)
44 | if err != nil {
45 | return err
46 | }
47 | defer k.Close()
48 |
49 | if err := k.DeleteValue("AutoConfigURL"); err != nil {
50 | return fmt.Errorf("delete AutoConfigURL: %v", err)
51 | }
52 |
53 | callInternetSetOption(internetOptionSettingsChanged)
54 | callInternetSetOption(internetOptionRefresh)
55 |
56 | return nil
57 | }
58 |
59 | func callInternetSetOption(dwOption int) {
60 | ret, _, err := internetSetOption.Call(0, uintptr(dwOption), 0, 0)
61 | if ret == 0 {
62 | log.Printf("failed to call InternetSetOption with option %d: %v", dwOption, err)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/internal/sysproxy/pac.go:
--------------------------------------------------------------------------------
1 | package sysproxy
2 |
3 | import (
4 | "bytes"
5 | _ "embed"
6 | "text/template"
7 | )
8 |
9 | var (
10 | pacTemplate = template.Must(
11 | template.New("pac").Parse(`function FindProxyForURL(url, host) {
12 | var excludedHosts = [{{range $index, $host := .ExcludedHosts}}{{if $index}},{{end}}"{{$host}}"{{end}}];
13 | for (var i = 0; i < excludedHosts.length; i++) {
14 | if (dnsDomainIs(host, excludedHosts[i])) {
15 | return "DIRECT";
16 | }
17 | }
18 | return "PROXY 127.0.0.1:{{.ProxyPort}}; DIRECT";
19 | }`))
20 |
21 | //go:embed exclusions/common.txt
22 | commonExcludedHosts []byte
23 | )
24 |
25 | // renderPac returns the PAC file content for the given proxy port and user-configured excluded hosts.
26 | func renderPac(proxyPort int, userConfiguredExcludedHosts []string) []byte {
27 | var buf bytes.Buffer
28 | pacTemplate.Execute(&buf, struct {
29 | ProxyPort int
30 | ExcludedHosts []string
31 | }{
32 | ProxyPort: proxyPort,
33 | ExcludedHosts: buildExcludedHosts(userConfiguredExcludedHosts),
34 | })
35 | return buf.Bytes()
36 | }
37 |
38 | // buildExcludedHosts returns a list of hosts that should be excluded from being proxied.
39 | // It combines common, platform-specific, and user-configured excluded hosts.
40 | func buildExcludedHosts(userConfiguredExcludedHosts []string) []string {
41 | var excludedHosts []string
42 |
43 | processList := func(data []byte) {
44 | for _, line := range bytes.Split(data, []byte("\n")) {
45 | if hashIndex := bytes.IndexByte(line, '#'); hashIndex != -1 {
46 | line = line[:hashIndex]
47 | }
48 | line = bytes.TrimSpace(line)
49 | if len(line) == 0 {
50 | continue
51 | }
52 | excludedHosts = append(excludedHosts, string(line))
53 | }
54 | }
55 |
56 | processList(commonExcludedHosts)
57 | processList(platformSpecificExcludedHosts)
58 | excludedHosts = append(excludedHosts, userConfiguredExcludedHosts...)
59 |
60 | return excludedHosts
61 | }
62 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | permissions:
11 | # These permissions are required by golangci-lint to create annotations for PRs.
12 | # https://github.com/golangci/golangci-lint-action?tab=readme-ov-file
13 | contents: read
14 | pull-requests: read
15 | checks: write
16 |
17 | jobs:
18 | lint-go:
19 | name: Lint Go (${{ matrix.os }})
20 | runs-on: ${{ matrix.os }}
21 | strategy:
22 | matrix:
23 | os: [ubuntu-latest, macos-latest, windows-latest]
24 | fail-fast: false
25 | steps:
26 | - uses: actions/checkout@v4
27 | - name: Set up Go
28 | uses: actions/setup-go@v5
29 | with:
30 | go-version-file: ./go.mod
31 | - name: Run go version
32 | run: go version
33 | - name: Create placeholder asset
34 | # go:embed in main.go requires the directory to exist and be non-empty
35 | run: |
36 | mkdir -p frontend/dist
37 | touch frontend/dist/placeholder
38 | - name: Run golangci-lint
39 | uses: golangci/golangci-lint-action@v7
40 | - name: Run govulncheck
41 | uses: golang/govulncheck-action@v1
42 |
43 | lint-frontend:
44 | name: Lint Frontend
45 | runs-on: ubuntu-latest
46 | steps:
47 | - uses: actions/checkout@v4
48 | - name: Set up Node
49 | uses: actions/setup-node@v4
50 | with:
51 | node-version-file: frontend/package.json
52 | cache: 'npm'
53 | cache-dependency-path: frontend/package-lock.json
54 | - name: Run node --version
55 | run: node --version
56 | - name: Set up Task
57 | uses: arduino/setup-task@v1
58 | with:
59 | version: 3.x
60 | repo-token: ${{ secrets.GITHUB_TOKEN }}
61 | - name: Install frontend dependencies
62 | run: npm ci
63 | working-directory: frontend
64 | - name: Run frontend linter
65 | run: task frontend:lint
66 |
--------------------------------------------------------------------------------
/frontend/i18next-parser.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // Where to look for keys
3 | input: ['src/**/*.{js,jsx,ts,tsx}'],
4 |
5 | // Where to write the translations
6 | output: 'src/i18n/locales/$LOCALE.json',
7 |
8 | // Your languages
9 | locales: ['en-US', 'de-DE', 'kk-KZ', 'ru-RU', 'it-IT', 'zh-TW', 'zh-CN', 'fr-FR'],
10 |
11 | // Set to false to disable saving old catalogs
12 | createOldCatalogs: false,
13 |
14 | // Use nested JSON
15 | namespaceSeparator: false,
16 | keySeparator: '.',
17 |
18 | // Sort keys alphabetically
19 | sort: true,
20 |
21 | // Don't add empty translations for every locale - just add the keys
22 | // This preserves existing translations
23 | keepRemoved: true,
24 |
25 | // Add source file references (helps for debugging)
26 | addTags: false,
27 |
28 | // Functions to look for
29 | lexers: {
30 | js: [
31 | {
32 | lexer: 'JsxLexer',
33 | functions: ['t'], // Look for t() function
34 | namespaceFunctions: ['useTranslation', 'withTranslation'], // Also track these
35 | },
36 | ],
37 | jsx: [
38 | {
39 | lexer: 'JsxLexer',
40 | functions: ['t'],
41 | namespaceFunctions: ['useTranslation', 'withTranslation'],
42 | },
43 | ],
44 | ts: [
45 | {
46 | lexer: 'JsxLexer',
47 | functions: ['t'],
48 | namespaceFunctions: ['useTranslation', 'withTranslation'],
49 | },
50 | ],
51 | tsx: [
52 | {
53 | lexer: 'JsxLexer',
54 | functions: ['t'],
55 | namespaceFunctions: ['useTranslation', 'withTranslation'],
56 | },
57 | ],
58 | },
59 |
60 | // Default value handling - for English, use the key itself
61 | defaultValue: function (locale, namespace, key) {
62 | if (locale === 'en_US') {
63 | return key;
64 | }
65 | return ''; // Empty string for other languages
66 | },
67 |
68 | // Output format
69 | indentation: 2,
70 |
71 | // Whether to add location data to JSON files
72 | lineEnding: 'auto',
73 |
74 | // Verbose output
75 | verbose: true,
76 | };
77 |
--------------------------------------------------------------------------------
/frontend/src/SettingsManager/IgnoredHostsInput.tsx:
--------------------------------------------------------------------------------
1 | import { FormGroup, TextArea, 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 { GetIgnoredHosts, SetIgnoredHosts } from '../../wailsjs/go/cfg/Config';
7 | import { useProxyState } from '../context/ProxyStateContext';
8 |
9 | export function IgnoredHostsInput() {
10 | const { t } = useTranslation();
11 | const { isProxyRunning } = useProxyState();
12 | const [state, setState] = useState({
13 | ignoredHosts: '',
14 | loading: true,
15 | });
16 |
17 | useEffect(() => {
18 | (async () => {
19 | const ignoredHosts = await GetIgnoredHosts();
20 | setState({ ignoredHosts: (ignoredHosts ?? []).join('\n'), loading: false });
21 | })();
22 | }, []);
23 |
24 | const setIgnoredHosts = useDebouncedCallback(async (ignoredHosts: string) => {
25 | await SetIgnoredHosts(
26 | ignoredHosts
27 | .split('\n')
28 | .map((host) => host.trim())
29 | .filter((host) => host.length > 0),
30 | );
31 | }, 500);
32 |
33 | return (
34 |
39 | {t('ignoredHostsInput.description')}
40 |
41 | {t('ignoredHostsInput.helper')}
42 | >
43 | }
44 | >
45 |
51 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/Rules/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, TextArea, 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 './index.css';
7 | import { GetRules, SetRules } from '../../wailsjs/go/cfg/Config';
8 | import { BrowserOpenURL } from '../../wailsjs/runtime/runtime';
9 | import { useProxyState } from '../context/ProxyStateContext';
10 |
11 | const HELP_URL = 'https://github.com/ZenPrivacy/zen-desktop/blob/master/docs/external/how-to-rules.md';
12 |
13 | export function Rules() {
14 | const { t } = useTranslation();
15 | const { isProxyRunning } = useProxyState();
16 | const [state, setState] = useState({
17 | rules: '',
18 | loading: true,
19 | });
20 |
21 | useEffect(() => {
22 | (async () => {
23 | const filters = await GetRules();
24 | if (filters !== null) {
25 | setState({ rules: filters.join('\n'), loading: false });
26 | } else {
27 | setState({ ...state, loading: false });
28 | }
29 | })();
30 | }, []);
31 |
32 | const setFilters = useDebouncedCallback(async (rules: string) => {
33 | await SetRules(
34 | rules
35 | .split('\n')
36 | .map((f) => f.trim())
37 | .filter((f) => f.length > 0),
38 | );
39 | }, 500);
40 |
41 | return (
42 |
43 |
44 |
47 |
48 |
54 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/cfg/Config.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {filter} from '../models';
4 | import {cfg} from '../models';
5 | import {sync} from '../models';
6 |
7 | export function AddFilterList(arg1:filter.List):Promise;
8 |
9 | export function AddFilterLists(arg1:Array):Promise;
10 |
11 | export function ExportDebugData():Promise;
12 |
13 | export function GetCAInstalled():Promise;
14 |
15 | export function GetFilterLists():Promise>;
16 |
17 | export function GetFilterListsByLocales(arg1:Array):Promise>;
18 |
19 | export function GetFirstLaunch():Promise;
20 |
21 | export function GetIgnoredHosts():Promise>;
22 |
23 | export function GetLocale():Promise;
24 |
25 | export function GetPACPort():Promise;
26 |
27 | export function GetPort():Promise;
28 |
29 | export function GetRules():Promise>;
30 |
31 | export function GetTargetTypeFilterLists(arg1:filter.ListType):Promise>;
32 |
33 | export function GetUpdatePolicy():Promise;
34 |
35 | export function GetVersion():Promise;
36 |
37 | export function Lock():Promise;
38 |
39 | export function RLock():Promise;
40 |
41 | export function RLocker():Promise;
42 |
43 | export function RUnlock():Promise;
44 |
45 | export function RemoveFilterList(arg1:string):Promise;
46 |
47 | export function RunMigrations():Promise;
48 |
49 | export function Save():Promise;
50 |
51 | export function SetCAInstalled(arg1:boolean):Promise;
52 |
53 | export function SetIgnoredHosts(arg1:Array):Promise;
54 |
55 | export function SetLocale(arg1:string):Promise;
56 |
57 | export function SetPort(arg1:number):Promise;
58 |
59 | export function SetRules(arg1:Array):Promise;
60 |
61 | export function SetUpdatePolicy(arg1:cfg.UpdatePolicyType):Promise;
62 |
63 | export function ToggleFilterList(arg1:string,arg2:boolean):Promise;
64 |
65 | export function TryLock():Promise;
66 |
67 | export function TryRLock():Promise;
68 |
69 | export function Unlock():Promise;
70 |
--------------------------------------------------------------------------------
/frontend/src/SettingsManager/AutostartSwitch/index.tsx:
--------------------------------------------------------------------------------
1 | import { Switch, FormGroup } from '@blueprintjs/core';
2 | import { useCallback, useEffect, useState } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import { IsEnabled, Enable, Disable } from '../../../wailsjs/go/autostart/Manager';
6 | import { AppToaster } from '../../common/toaster';
7 |
8 | export function AutostartSwitch() {
9 | const { t } = useTranslation();
10 | const [state, setState] = useState({
11 | enabled: false,
12 | loading: true,
13 | });
14 |
15 | useEffect(() => {
16 | (async () => {
17 | const enabled = await IsEnabled();
18 | setState({ ...state, enabled, loading: false });
19 | })();
20 | }, []);
21 |
22 | const disable = useCallback(() => {
23 | (async () => {
24 | setState((state) => ({ ...state, loading: true }));
25 | try {
26 | await Disable();
27 | } catch (err) {
28 | AppToaster.show({
29 | message: t('autoStartSwitch.disableError', { error: err }),
30 | intent: 'danger',
31 | });
32 | setState((state) => ({ ...state, loading: false }));
33 | return;
34 | }
35 | setState((state) => ({ ...state, enabled: false, loading: false }));
36 | })();
37 | }, []);
38 | const enable = useCallback(() => {
39 | (async () => {
40 | setState((state) => ({ ...state, loading: true }));
41 | try {
42 | await Enable();
43 | } catch (err) {
44 | AppToaster.show({
45 | message: t('autoStartSwitch.enableError', { error: err }),
46 | intent: 'danger',
47 | });
48 | setState((state) => ({ ...state, loading: false }));
49 | return;
50 | }
51 | setState((state) => ({ ...state, enabled: true, loading: false }));
52 | })();
53 | }, []);
54 |
55 | return (
56 |
57 | {
63 | if (state.enabled) {
64 | disable();
65 | } else {
66 | enable();
67 | }
68 | }}
69 | />
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/frontend/src/assets/icons/reddit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/SettingsManager/AutoupdateSwitch/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormGroup, Switch } from '@blueprintjs/core';
2 | import { useCallback, useEffect, useState } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import { GetUpdatePolicy, SetUpdatePolicy } from '../../../wailsjs/go/cfg/Config';
6 | import { cfg } from '../../../wailsjs/go/models';
7 | import { AppToaster } from '../../common/toaster';
8 |
9 | export function AutoupdateSwitch() {
10 | const { t } = useTranslation();
11 | const [state, setState] = useState({
12 | enabled: false,
13 | });
14 |
15 | useEffect(() => {
16 | (async () => {
17 | const policy = await GetUpdatePolicy();
18 | setState({
19 | enabled: policy === cfg.UpdatePolicyType.AUTOMATIC,
20 | });
21 | })();
22 | }, []);
23 |
24 | const disable = useCallback(() => {
25 | (async () => {
26 | setState((state) => ({ ...state, loading: true }));
27 | try {
28 | await SetUpdatePolicy(cfg.UpdatePolicyType.DISABLED);
29 | } catch (err) {
30 | AppToaster.show({
31 | message: t('settings.updates.disableError', { error: err }),
32 | intent: 'danger',
33 | });
34 | setState((state) => ({ ...state, loading: false }));
35 | return;
36 | }
37 | setState((state) => ({ ...state, enabled: false, loading: false }));
38 | })();
39 | }, []);
40 | const enable = useCallback(() => {
41 | (async () => {
42 | setState((state) => ({ ...state, loading: true }));
43 | try {
44 | await SetUpdatePolicy(cfg.UpdatePolicyType.AUTOMATIC);
45 | } catch (err) {
46 | AppToaster.show({
47 | message: t('settings.updates.enableError', { error: err }),
48 | intent: 'danger',
49 | });
50 | setState((state) => ({ ...state, loading: false }));
51 | return;
52 | }
53 | setState((state) => ({ ...state, enabled: true, loading: false }));
54 | })();
55 | }, []);
56 |
57 | return (
58 |
59 | {
64 | if (state.enabled) {
65 | disable();
66 | } else {
67 | enable();
68 | }
69 | }}
70 | />
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Report a bug in Zen
3 | type: Bug
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thank you for taking the time to report a bug in Zen.
9 | Before submitting this issue, please search the [issue tracker](https://github.com/ZenPrivacy/zen-desktop/issues?q=is%3Aissue) to see if it has already been reported.
10 | Please fill out the information below to help us reproduce and fix the issue you are experiencing.
11 | - type: textarea
12 | id: description
13 | attributes:
14 | label: Description
15 | description: |
16 | Describe the bug you are experiencing in as much detail as possible.
17 | Attach screenshots or videos if it helps explain the issue.
18 | In case you are proficient with using the command line, and use macOS or Linux, please include the logs that Zen prints out ([read how to locate the binaries on our wiki](https://github.com/ZenPrivacy/zen-desktop/wiki/How-to-find-Zen's-binaries)).
19 | validations:
20 | required: true
21 | - type: input
22 | id: version
23 | attributes:
24 | label: Version
25 | description: |
26 | What version of Zen are you using? It can be found on the bottom of the "Settings" page.
27 | placeholder: v0.0.0
28 | validations:
29 | required: true
30 | - type: input
31 | id: os-kind
32 | attributes:
33 | label: Operating System
34 | description: |
35 | What operating system and version number are you using? If you are using Linux, please include the distribution.
36 | placeholder: Windows 10
37 | validations:
38 | required: true
39 | - type: textarea
40 | id: steps
41 | attributes:
42 | label: Steps to Reproduce
43 | description: |
44 | If you are able to reproduce the bug consistently, please include the steps to reproduce it.
45 | placeholder: |
46 | 1. Open the app
47 | 2. Click on the "Start" button
48 | 3 ...
49 | validations:
50 | required: false
51 | - type: textarea
52 | id: additional-context
53 | attributes:
54 | label: Additional Context
55 | description: Any additional information that might be relevant to the issue.
56 | placeholder: |
57 | Are you using any other tunnels, proxies or VPNs? Are you using any antivirus software? Which apps or websites seem to be affected?
58 | validations:
59 | required: false
60 |
--------------------------------------------------------------------------------
/internal/sysproxy/exclusions/common.txt:
--------------------------------------------------------------------------------
1 | # This file contains hostnames that Zen will not MITM on all platforms.
2 |
3 | # Sensitive
4 | proton.me
5 |
6 | # Login
7 | auth.openai.com
8 | accounts.google.com
9 | login.coinbase.com
10 | appleid.apple.com
11 | account.apple.com
12 | accounts.binance.com
13 | connexion-mabanque.bnpparibas
14 |
15 | # Government websites
16 | gouv.fr
17 | egov.kz
18 | gov.kz
19 | gov.uk
20 | gov.ua
21 | usa.gov
22 | state.gov
23 | whitehouse.gov
24 | gov.ph
25 | gov.au
26 | e-estonia.com
27 | eesti.ee
28 |
29 | # Password managers
30 | 1password.com
31 | lastpass.com
32 | passwords.google.com
33 | passwords.google
34 | bitwarden.com
35 | dashlane.com
36 |
37 | # Banks & financial institutions
38 | mabanque.bnpparibas
39 | credit-agricole.fr
40 | creditmutuel.fr
41 | sg.fr
42 | bankofamerica.com
43 | chase.com
44 | wellsfargo.com
45 | citibank.com
46 | usbank.com
47 | capitalone.com
48 | pnc.com
49 | revolut.com
50 | monzo.com
51 | kaspi.kz
52 | halykbank.kz
53 | bcc.kz
54 | bankffin.kz
55 | jusan.kz
56 | onlinebank.kz
57 | freedompay.kz
58 | wlp-acs.com
59 |
60 | # Payment processors
61 | paypal.com
62 | stripe.com
63 | m.stripe.network
64 | square.com
65 | venmo.com
66 | sis.redsys.es
67 | payu.com
68 | pay.amazon.com
69 | klarna.com
70 |
71 | # Messengers
72 | whatsapp.net
73 | whatsapp.com
74 | telegram.org
75 | signal.org
76 | discord.com
77 | slack.com
78 | messenger.com
79 |
80 | # Digital infrastructure
81 | cloudflare.com
82 | aws.amazon.com
83 | azure.microsoft.com
84 | cloud.google.com
85 | hetzner.com
86 | digitalocean.com
87 | linode.com
88 | vultr.com
89 | heroku.com
90 | netlify.com
91 | vercel.com
92 | fastly.com
93 | akamai.com
94 | ps.kz
95 | github.com
96 | gitlab.com
97 | bitbucket.org
98 | docker.com
99 | kubernetes.io
100 | npmjs.com
101 | pypi.org
102 | crates.io
103 | rubygems.org
104 | signpath.io
105 | developer.apple.com
106 | auth0.com
107 | hanko.io
108 | clerk.com
109 | workos.com
110 | zitadel.com
111 | stytch.com
112 | bunny.net
113 | dash.irbis.sh
114 |
115 | # OpenAI from here: https://help.openai.com/en/articles/9247338-network-recommendations-for-chatgpt-errors-on-web-and-apps
116 | ios.chat.openai.com
117 | ab.chatgpt.com
118 | oaiusercontent.com
119 | ws.chatgpt.com
120 |
121 | # Issues
122 | # https://github.com/ZenPrivacy/zen-desktop/pull/223
123 | account.booking.com
124 | # https://github.com/ZenPrivacy/zen-desktop/issues/407
125 | updates.ghub.logitechg.com
126 |
--------------------------------------------------------------------------------
/frontend/src/assets/icons/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 |
3 | ## Bug Reports
4 |
5 | Zen provides a [template](https://github.com/ZenPrivacy/zen-desktop/issues/new?assignees=&labels=bug&projects=&template=bug_report.yaml) that you can use to file a detailed bug report. Please follow the instructions in the template and provide as much information as possible. We want to help, but we can only do so if we have sufficient context to work with.
6 |
7 | ## Feature Requests
8 |
9 | Feature requests are welcome! Please use the [template](https://github.com/ZenPrivacy/zen-desktop/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yaml) that will guide you through the information we need to consider your request. Take a moment to consider whether your idea aligns with the scope and goals of the project. Please note that it's up to you to make a compelling case for your proposed feature.
10 |
11 | ## Translations
12 |
13 | We welcome contributions to translations! If you'd like to add a new language or improve an existing one, please follow the [localization guide](docs/internal/localization-guide.md). It will walk you through how to add or update translations in Zen.
14 |
15 | ## Pull Requests
16 |
17 | Thoughtful pull requests (patches, improvements, new features) are always welcome. That said, **please ask first** before starting any significant work.
18 |
19 | Here are a few ways to get involved:
20 |
21 | - Look for issues labeled [`good first issue`](https://github.com/ZenPrivacy/zen-desktop/labels/good%20first%20issue) or [`help wanted`](https://github.com/ZenPrivacy/zen-desktop/labels/help%20wanted). If you’d like to work on one, **explicitly ask to be assigned and briefly describe your plan**. This helps us avoid duplicate efforts and ensures you don't risk spending time on something that might not be accepted into the project.
22 | - If you have an idea for a new feature or bug fix, open an issue to discuss it first. Clearly state your intention to work on it and provide a short summary of how you plan to approach the problem. This will help us understand your intentions and offer guidance if needed. **Please wait for feedback** before you begin development.
23 |
24 | If you plan to contribute to development, start by reading the [Getting Started](docs/internal/index.md#getting-started) section of the internal documentation. It will help you set up your environment and get familiar with the project.
25 |
26 | > [!IMPORTANT]
27 | > By contributing code or any other assets, you agree that your work will be licensed under the MIT License as [used by the project](https://github.com/ZenPrivacy/zen-desktop/blob/master/LICENSE).
28 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "os"
9 | "runtime"
10 |
11 | "github.com/ZenPrivacy/zen-desktop/internal/app"
12 | "github.com/ZenPrivacy/zen-desktop/internal/autostart"
13 | "github.com/ZenPrivacy/zen-desktop/internal/cfg"
14 | "github.com/ZenPrivacy/zen-desktop/internal/constants"
15 | "github.com/ZenPrivacy/zen-desktop/internal/logger"
16 | "github.com/wailsapp/wails/v2"
17 | "github.com/wailsapp/wails/v2/pkg/options"
18 | "github.com/wailsapp/wails/v2/pkg/options/assetserver"
19 | "github.com/wailsapp/wails/v2/pkg/options/mac"
20 | )
21 |
22 | //go:embed all:frontend/dist
23 | var assets embed.FS
24 |
25 | func main() {
26 | startOnDomReady := flag.Bool("start", false, "Start the service when DOM is ready")
27 | startHidden := flag.Bool("hidden", false, "Start the application in hidden mode")
28 | uninstallCA := flag.Bool("uninstall-ca", false, "Uninstall the CA and exit")
29 | flag.Parse()
30 |
31 | err := logger.SetupLogger()
32 | if err != nil {
33 | log.Printf("failed to setup logger: %v", err)
34 | }
35 | log.Printf("initializing the app; version=%q", cfg.Version)
36 |
37 | config, err := cfg.NewConfig()
38 | if err != nil {
39 | log.Fatalf("failed to load config: %v", err)
40 | }
41 |
42 | app, err := app.NewApp(constants.AppName, config, *startOnDomReady)
43 | if err != nil {
44 | log.Fatalf("failed to create app: %v", err)
45 | }
46 |
47 | if *uninstallCA {
48 | if err := app.UninstallCA(); err != nil {
49 | // UninstallCA logs the error internally
50 | os.Exit(1)
51 | }
52 |
53 | log.Println("CA uninstalled successfully")
54 | return
55 | }
56 |
57 | autostart := &autostart.Manager{}
58 |
59 | err = wails.Run(&options.App{
60 | Title: constants.AppName,
61 | Width: 420,
62 | Height: 650,
63 | DisableResize: true,
64 | AssetServer: &assetserver.Options{
65 | Assets: assets,
66 | },
67 | OnStartup: app.Startup,
68 | OnBeforeClose: app.BeforeClose,
69 | SingleInstanceLock: &options.SingleInstanceLock{
70 | UniqueId: constants.InstanceID,
71 | OnSecondInstanceLaunch: app.OnSecondInstanceLaunch,
72 | },
73 | Bind: []interface{}{
74 | app,
75 | config,
76 | autostart,
77 | },
78 | EnumBind: []interface{}{
79 | cfg.UpdatePolicyEnum,
80 | },
81 | Mac: &mac.Options{
82 | About: &mac.AboutInfo{
83 | Title: constants.AppName,
84 | Message: fmt.Sprintf("Your Comprehensive Ad-Blocker and Privacy Guard\nVersion: %s\n© 2025 Zen Privacy Project Developers", cfg.Version),
85 | },
86 | },
87 | HideWindowOnClose: runtime.GOOS == "darwin" || runtime.GOOS == "windows",
88 | StartHidden: *startHidden,
89 | })
90 |
91 | if err != nil {
92 | log.Fatal(err)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/frontend/src/common/ThemeManager.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, createContext, useContext, ReactNode, useMemo } from 'react';
2 |
3 | import { WindowSetDarkTheme, WindowSetLightTheme, WindowSetSystemDefaultTheme } from '../../wailsjs/runtime/runtime';
4 |
5 | export enum ThemeType {
6 | SYSTEM = 'system',
7 | LIGHT = 'light',
8 | DARK = 'dark',
9 | }
10 |
11 | interface ThemeContextType {
12 | theme: ThemeType;
13 | effectiveTheme: ThemeType.DARK | ThemeType.LIGHT;
14 | setTheme: (theme: ThemeType) => void;
15 | }
16 |
17 | const ThemeContext = createContext(undefined);
18 |
19 | const STORAGE_KEY = 'zen::theme';
20 |
21 | export function ThemeProvider({ children }: { children: ReactNode }) {
22 | const [theme, setThemeState] = useState(() => {
23 | const savedTheme = localStorage.getItem(STORAGE_KEY);
24 | return (savedTheme as ThemeType) || ThemeType.SYSTEM;
25 | });
26 |
27 | const [effectiveTheme, setEffectiveTheme] = useState(ThemeType.DARK);
28 |
29 | const setTheme = (newTheme: ThemeType) => {
30 | setThemeState(newTheme);
31 | localStorage.setItem(STORAGE_KEY, newTheme);
32 | switch (newTheme) {
33 | case 'light':
34 | WindowSetLightTheme();
35 | break;
36 | case 'dark':
37 | WindowSetDarkTheme();
38 | break;
39 | default:
40 | WindowSetSystemDefaultTheme();
41 | }
42 | };
43 |
44 | useEffect(() => {
45 | if (theme !== ThemeType.SYSTEM) {
46 | setEffectiveTheme(theme);
47 | return () => {};
48 | }
49 |
50 | const syncSystemTheme = () => {
51 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
52 | setEffectiveTheme(prefersDark ? ThemeType.DARK : ThemeType.LIGHT);
53 | };
54 |
55 | syncSystemTheme();
56 |
57 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
58 |
59 | if (mediaQuery.addEventListener) {
60 | mediaQuery.addEventListener('change', syncSystemTheme);
61 | } else {
62 | mediaQuery.addListener(syncSystemTheme);
63 | }
64 |
65 | return () => {
66 | if (mediaQuery.removeEventListener) {
67 | mediaQuery.removeEventListener('change', syncSystemTheme);
68 | } else {
69 | mediaQuery.removeListener(syncSystemTheme);
70 | }
71 | };
72 | }, [theme]);
73 |
74 | const value = useMemo(() => ({ theme, effectiveTheme, setTheme }), [theme, effectiveTheme]);
75 |
76 | return {children};
77 | }
78 |
79 | export function useTheme() {
80 | const context = useContext(ThemeContext);
81 | if (context === undefined) {
82 | throw new Error('useTheme must be used within a ThemeProvider');
83 | }
84 | return context;
85 | }
86 |
--------------------------------------------------------------------------------
/internal/sysproxy/system_darwin.go:
--------------------------------------------------------------------------------
1 | package sysproxy
2 |
3 | import (
4 | "bytes"
5 | _ "embed"
6 | "errors"
7 | "fmt"
8 | "os/exec"
9 | "strings"
10 |
11 | "github.com/hashicorp/go-multierror"
12 | )
13 |
14 | var (
15 | //go:embed exclusions/darwin.txt
16 | platformSpecificExcludedHosts []byte
17 |
18 | // networkServices remembers the services we modified, for unsetSystemProxy.
19 | networkServices []string
20 | )
21 |
22 | // setSystemProxy sets the system proxy PAC URL.
23 | func setSystemProxy(pacURL string) error {
24 | svcs, err := discoverNetworkServices()
25 | if err != nil {
26 | return fmt.Errorf("discover network services: %v", err)
27 | }
28 | networkServices = svcs
29 |
30 | for _, svc := range networkServices {
31 | cmd := exec.Command("networksetup", "-setwebproxystate", svc, "off")
32 | if out, err := cmd.CombinedOutput(); err != nil {
33 | return fmt.Errorf("unset web proxy for network service %q: %v (%q)", svc, err, out)
34 | }
35 |
36 | cmd = exec.Command("networksetup", "-setsecurewebproxystate", svc, "off")
37 | if out, err := cmd.CombinedOutput(); err != nil {
38 | return fmt.Errorf("unset secure web proxy for network service %q: %v (%q)", svc, err, out)
39 | }
40 |
41 | cmd = exec.Command("networksetup", "-setautoproxyurl", svc, pacURL)
42 | if out, err := cmd.CombinedOutput(); err != nil {
43 | return fmt.Errorf("set autoproxyurl to %q for network service %q: %v (%q)", pacURL, svc, err, out)
44 | }
45 | }
46 |
47 | return nil
48 | }
49 |
50 | func unsetSystemProxy() error {
51 | if len(networkServices) == 0 {
52 | return nil
53 | }
54 |
55 | var result error
56 | for _, svc := range networkServices {
57 | cmd := exec.Command("networksetup", "-setautoproxystate", svc, "off")
58 | if out, err := cmd.CombinedOutput(); err != nil {
59 | result = multierror.Append(result, fmt.Errorf("set autoproxystate to off for network service %q: %v (%q)", svc, err, out))
60 | }
61 | }
62 |
63 | networkServices = nil
64 | return result
65 | }
66 |
67 | // discoverNetworkServices returns a list of all network service names.
68 | func discoverNetworkServices() ([]string, error) {
69 | cmd := exec.Command("networksetup", "-listallnetworkservices")
70 | out, err := cmd.CombinedOutput()
71 | if err != nil {
72 | return nil, fmt.Errorf("list network services: %v (%q)", err, out)
73 | }
74 |
75 | lines := bytes.Split(out, []byte{'\n'})
76 | if len(lines) < 2 {
77 | return nil, errors.New("no network services found")
78 | }
79 |
80 | // The first line contains "An asterisk (*) denotes that a network service is disabled."
81 | services := make([]string, 0, len(lines)-1)
82 | for _, raw := range lines[1:] {
83 | line := strings.TrimSpace(string(raw))
84 | if line == "" {
85 | continue
86 | }
87 | if line[0] == '*' {
88 | // Disabled service; remove the asterisk.
89 | line = strings.TrimSpace(line[1:])
90 | }
91 |
92 | services = append(services, line)
93 | }
94 |
95 | return services, nil
96 | }
97 |
--------------------------------------------------------------------------------
/frontend/src/Intro/ConnectScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Divider } from '@blueprintjs/core';
2 | import { Trans, useTranslation } from 'react-i18next';
3 |
4 | import { BrowserOpenURL } from '../../../wailsjs/runtime/runtime';
5 | import BlueSkyLogo from '../../assets/icons/bluesky.svg';
6 | import DiscordIcon from '../../assets/icons/discord.svg';
7 | import GithubIcon from '../../assets/icons/github.svg';
8 | import OpenCollectiveIcon from '../../assets/icons/osc.svg';
9 | import RedditIcon from '../../assets/icons/reddit.svg';
10 | import { SOCIAL_LINKS } from '../../constants/urls';
11 |
12 | import './index.css';
13 |
14 | export function ConnectScreen() {
15 | const { t } = useTranslation();
16 |
17 | return (
18 |
19 |
{t('intro.connect.title')}
20 |
{t('intro.connect.description')}
21 |
22 | ,
26 | br:
,
27 | }}
28 | />
29 |
30 |
31 |
32 |
33 | {t('intro.connect.socialText')}
34 |
35 |
36 |
37 |
38 |
42 |
43 |
47 |
48 |
49 |
50 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
64 | {t('intro.connect.donateText')}
65 | }
67 | onClick={() => BrowserOpenURL(SOCIAL_LINKS.OPEN_COLLECTIVE)}
68 | >
69 | Open Collective
70 |
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/frontend/src/SettingsManager/UninstallCADialog/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Dialog, DialogBody, DialogFooter, Tooltip } from '@blueprintjs/core';
2 | import { useState } from 'react';
3 | import { Trans, useTranslation } from 'react-i18next';
4 |
5 | import './index.css';
6 |
7 | import { UninstallCA } from '../../../wailsjs/go/app/App';
8 | import { AppToaster } from '../../common/toaster';
9 | import { ProxyState } from '../../types';
10 |
11 | export interface UninstallCADialogProps {
12 | proxyState: ProxyState;
13 | }
14 | export function UninstallCADialog({ proxyState }: UninstallCADialogProps) {
15 | const { t } = useTranslation();
16 | const [state, setState] = useState({
17 | isOpen: false,
18 | loading: false,
19 | });
20 |
21 | return (
22 | <>
23 |
24 |
32 |
33 |
34 |
57 |
58 | {
64 | setState((state) => ({ ...state, loading: true }));
65 | try {
66 | await UninstallCA();
67 | AppToaster.show({
68 | message: t('settings.ca.successMessage'),
69 | intent: 'success',
70 | });
71 | } catch (err) {
72 | AppToaster.show({
73 | message: t('settings.ca.errorMessage', { error: err }),
74 | intent: 'danger',
75 | });
76 | } finally {
77 | setState((state) => ({ ...state, isOpen: false, loading: false }));
78 | }
79 | }}
80 | >
81 | {t('settings.ca.uninstallConfirm')}
82 |
83 | }
84 | />
85 |
86 | >
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/internal/autostart/autostart_windows.go:
--------------------------------------------------------------------------------
1 | // autostart_windows.go provides autostart capabilities for Windows.
2 | // To add the app to autostart, it creates a registry key under HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run.
3 | //
4 | // References:
5 | // - https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys
6 |
7 | package autostart
8 |
9 | import (
10 | "errors"
11 | "fmt"
12 | "log"
13 | "strings"
14 |
15 | "github.com/ZenPrivacy/zen-desktop/internal/constants"
16 | "golang.org/x/sys/windows/registry"
17 | )
18 |
19 | const (
20 | regKey = constants.AppName
21 | regPath = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
22 | )
23 |
24 | func (m Manager) IsEnabled() (enabled bool, err error) {
25 | defer func() {
26 | if err != nil {
27 | log.Printf("error checking registry key: %s", err)
28 | }
29 | }()
30 |
31 | key, err := registry.OpenKey(registry.CURRENT_USER, regPath, registry.QUERY_VALUE)
32 | if err != nil {
33 | return false, fmt.Errorf("open registry key: %w", err)
34 | }
35 | defer key.Close()
36 |
37 | execPath, err := getExecPath()
38 | if err != nil {
39 | return false, fmt.Errorf("get exec path: %w", err)
40 | }
41 |
42 | value, _, err := key.GetStringValue(regKey)
43 | switch {
44 | case errors.Is(err, registry.ErrNotExist) || errors.Is(err, registry.ErrUnexpectedType):
45 | return false, nil
46 | case err != nil:
47 | return false, fmt.Errorf("get string value: %w", err)
48 | }
49 |
50 | return strings.HasPrefix(value, execPath), nil // Use strings.HasPrefix to account for --start and any other future potential cli flags.
51 | }
52 |
53 | func (m Manager) Enable() (err error) {
54 | defer func() {
55 | if err != nil {
56 | log.Printf("error enabling autostart: %s", err)
57 | }
58 | }()
59 |
60 | if enabled, err := m.IsEnabled(); err != nil {
61 | return fmt.Errorf("check enabled: %w", err)
62 | } else if enabled {
63 | return nil
64 | }
65 |
66 | execPath, err := getExecPath()
67 | if err != nil {
68 | return fmt.Errorf("get exec path: %w", err)
69 | }
70 |
71 | key, err := registry.OpenKey(registry.CURRENT_USER, regPath, registry.WRITE)
72 | if err != nil {
73 | return fmt.Errorf("open registry key: %w", err)
74 | }
75 | defer key.Close()
76 |
77 | cmd := execPath + " " + "--start" + " " + "--hidden"
78 |
79 | if err := key.SetStringValue(regKey, cmd); err != nil {
80 | return fmt.Errorf("set string value: %w", err)
81 | }
82 |
83 | return nil
84 | }
85 |
86 | func (m Manager) Disable() (err error) {
87 | defer func() {
88 | if err != nil {
89 | log.Printf("error disabling autostart: %s", err)
90 | }
91 | }()
92 |
93 | if enabled, err := m.IsEnabled(); err != nil {
94 | return fmt.Errorf("check enabled: %w", err)
95 | } else if !enabled {
96 | return nil
97 | }
98 |
99 | key, err := registry.OpenKey(registry.CURRENT_USER, regPath, registry.SET_VALUE)
100 | if err != nil {
101 | return fmt.Errorf("open registry key: %w", err)
102 | }
103 | defer key.Close()
104 |
105 | if err := key.DeleteValue(regKey); err != nil {
106 | return fmt.Errorf("delete value: %w", err)
107 | }
108 |
109 | return nil
110 | }
111 |
--------------------------------------------------------------------------------
/internal/systray/manager_windows.go:
--------------------------------------------------------------------------------
1 | package systray
2 |
3 | import (
4 | "context"
5 | "embed"
6 | "errors"
7 | "fmt"
8 | "log"
9 | "sync"
10 |
11 | "github.com/wailsapp/wails/v2/pkg/runtime"
12 | )
13 |
14 | //go:embed logo.ico
15 | var logoFS embed.FS
16 |
17 | type Manager struct {
18 | logoBytes []byte
19 | appName string
20 | proxyStateMu sync.Mutex
21 | proxyActive bool
22 | proxyStart func()
23 | proxyStop func()
24 | startStopMenuItem *menuItem
25 | }
26 |
27 | func NewManager(appName string, proxyStart func(), proxyStop func()) (*Manager, error) {
28 | if appName == "" {
29 | return nil, errors.New("appName is empty")
30 | }
31 | if proxyStart == nil {
32 | return nil, errors.New("proxyStart is nil")
33 | }
34 | if proxyStop == nil {
35 | return nil, errors.New("proxyStop is nil")
36 | }
37 |
38 | logoBytes, err := logoFS.ReadFile("logo.ico")
39 | if err != nil {
40 | return nil, fmt.Errorf("read logo from embed: %w", err)
41 | }
42 |
43 | return &Manager{
44 | logoBytes: logoBytes,
45 | proxyStart: proxyStart,
46 | proxyStop: proxyStop,
47 | appName: appName,
48 | }, nil
49 | }
50 |
51 | func (m *Manager) Init(ctx context.Context) error {
52 | go func() {
53 | run(m.onReady(ctx), nil)
54 | }()
55 |
56 | return nil
57 | }
58 |
59 | // Quit needs to be called on application quit.
60 | func (m *Manager) Quit() {
61 | quit()
62 | }
63 |
64 | // OnProxyStarted should be called when the proxy gets started.
65 | func (m *Manager) OnProxyStarted() {
66 | m.proxyStateMu.Lock()
67 | defer m.proxyStateMu.Unlock()
68 | m.proxyActive = true
69 |
70 | if m.startStopMenuItem == nil {
71 | // Sanity check.
72 | log.Println("startStopMenuItem is nil")
73 | return
74 | }
75 |
76 | m.startStopMenuItem.SetTitle("Stop")
77 | m.startStopMenuItem.SetTooltip("Stop")
78 | }
79 |
80 | // OnProxyStopped should be called when the proxy gets stopped.
81 | func (m *Manager) OnProxyStopped() {
82 | m.proxyStateMu.Lock()
83 | defer m.proxyStateMu.Unlock()
84 | m.proxyActive = false
85 |
86 | if m.startStopMenuItem == nil {
87 | // Sanity check.
88 | log.Println("startStopMenuItem is nil")
89 | return
90 | }
91 |
92 | m.startStopMenuItem.SetTitle("Start")
93 | m.startStopMenuItem.SetTooltip("Start")
94 | }
95 |
96 | func (m *Manager) onReady(ctx context.Context) func() {
97 | return func() {
98 | setIcon(m.logoBytes)
99 | setTooltip(m.appName)
100 |
101 | openMenuItem := addMenuItem("Open", "Open the application window")
102 | go func() {
103 | for range openMenuItem.ClickedCh {
104 | runtime.Show(ctx)
105 | }
106 | }()
107 |
108 | m.startStopMenuItem = addMenuItem("Start", "Start")
109 | go func() {
110 | for range m.startStopMenuItem.ClickedCh {
111 | m.proxyStateMu.Lock()
112 | active := m.proxyActive
113 | m.proxyStateMu.Unlock()
114 | if active {
115 | m.proxyStop()
116 | } else {
117 | m.proxyStart()
118 | }
119 | }
120 | }()
121 |
122 | addSeparator()
123 |
124 | quitMenuItem := addMenuItem("Quit", "Quit the application")
125 | go func() {
126 | for range quitMenuItem.ClickedCh {
127 | runtime.Quit(ctx)
128 | }
129 | }()
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ZenPrivacy/zen-desktop
2 |
3 | // Note: Always set the patch version when updating the Go version.
4 | // Omitting the patch version may cause some Go commands to fail.
5 | // For more details, see: https://go.dev/doc/toolchain#version
6 | go 1.25.3
7 |
8 | require (
9 | github.com/ZenPrivacy/zen-core v1.1.0
10 | github.com/blang/semver v3.5.1+incompatible
11 | github.com/hashicorp/go-multierror v1.1.1
12 | github.com/wailsapp/wails/v2 v2.10.2
13 | golang.org/x/sys v0.38.0
14 | gopkg.in/natefinch/lumberjack.v2 v2.2.1
15 | )
16 |
17 | require (
18 | github.com/andybalholm/brotli v1.2.0 // indirect
19 | github.com/bep/debounce v1.2.1 // indirect
20 | github.com/getlantern/byteexec v0.0.0-20220903142956-e6ed20032cfd // indirect
21 | github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
22 | github.com/getlantern/elevate v0.0.0-20220903142053-479ab992b264 // indirect
23 | github.com/getlantern/errors v1.0.3 // indirect
24 | github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c // indirect
25 | github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
26 | github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
27 | github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
28 | github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c // indirect
29 | github.com/go-logr/logr v1.2.3 // indirect
30 | github.com/go-logr/stdr v1.2.2 // indirect
31 | github.com/go-ole/go-ole v1.3.0 // indirect
32 | github.com/go-stack/stack v1.8.1 // indirect
33 | github.com/godbus/dbus/v5 v5.1.0 // indirect
34 | github.com/google/uuid v1.6.0 // indirect
35 | github.com/gorilla/websocket v1.5.3 // indirect
36 | github.com/hashicorp/errwrap v1.0.0 // indirect
37 | github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb // indirect
38 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
39 | github.com/klauspost/compress v1.18.0 // indirect
40 | github.com/labstack/echo/v4 v4.13.3 // indirect
41 | github.com/labstack/gommon v0.4.2 // indirect
42 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
43 | github.com/leaanthony/gosod v1.0.4 // indirect
44 | github.com/leaanthony/slicer v1.6.0 // indirect
45 | github.com/leaanthony/u v1.1.1 // indirect
46 | github.com/mattn/go-colorable v0.1.13 // indirect
47 | github.com/mattn/go-isatty v0.0.20 // indirect
48 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
49 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
50 | github.com/pkg/errors v0.9.1 // indirect
51 | github.com/rivo/uniseg v0.4.7 // indirect
52 | github.com/samber/lo v1.49.1 // indirect
53 | github.com/spyzhov/ajson v0.9.6 // indirect
54 | github.com/tdewolff/parse/v2 v2.8.4 // indirect
55 | github.com/tkrajina/go-reflector v0.5.8 // indirect
56 | github.com/valyala/bytebufferpool v1.0.0 // indirect
57 | github.com/valyala/fasttemplate v1.2.2 // indirect
58 | github.com/wailsapp/go-webview2 v1.0.19 // indirect
59 | github.com/wailsapp/mimetype v1.4.1 // indirect
60 | go.opentelemetry.io/otel v1.14.0 // indirect
61 | go.opentelemetry.io/otel/trace v1.14.0 // indirect
62 | go.uber.org/multierr v1.11.0 // indirect
63 | go.uber.org/zap v1.26.0 // indirect
64 | golang.org/x/crypto v0.45.0 // indirect
65 | golang.org/x/net v0.47.0 // indirect
66 | golang.org/x/text v0.31.0 // indirect
67 | howett.net/plist v1.0.1 // indirect
68 | )
69 |
--------------------------------------------------------------------------------
/frontend/src/StartStopButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@blueprintjs/core';
2 | import { useEffect } from 'react';
3 | import { Trans, useTranslation } from 'react-i18next';
4 |
5 | import { StartProxy, StopProxy } from '../wailsjs/go/app/App';
6 | import { EventsOn } from '../wailsjs/runtime/runtime';
7 |
8 | import { BrowserLink } from './common/BrowserLink';
9 | import { AppToaster } from './common/toaster';
10 | import { useProxyState } from './context/ProxyStateContext';
11 |
12 | const PROXY_CHANNEL = 'proxy:action';
13 | const LINUX_PROXY_GUIDE_URL = 'https://github.com/ZenPrivacy/zen-desktop/blob/master/docs/external/linux-proxy-conf.md';
14 |
15 | enum ProxyActionKind {
16 | Starting = 'starting',
17 | Started = 'started',
18 | StartError = 'startError',
19 | Stopping = 'stopping',
20 | Stopped = 'stopped',
21 | StopError = 'stopError',
22 | UnsupportedDE = 'unsupportedDE',
23 | }
24 |
25 | interface ProxyAction {
26 | kind: ProxyActionKind;
27 | error?: string;
28 | }
29 |
30 | export function StartStopButton() {
31 | const { t } = useTranslation();
32 | const { proxyState, setProxyState } = useProxyState();
33 |
34 | useEffect(() => {
35 | const cancel = EventsOn(PROXY_CHANNEL, (action: ProxyAction) => {
36 | switch (action.kind) {
37 | case ProxyActionKind.Starting:
38 | setProxyState('loading');
39 | break;
40 | case ProxyActionKind.Started:
41 | setProxyState('on');
42 | break;
43 | case ProxyActionKind.StartError:
44 | AppToaster.show({
45 | message: t('startStopButton.startError', { error: action.error }),
46 | intent: 'danger',
47 | });
48 | setProxyState('on'); // Still worth it to give the option to shut down in case the error is recoverable.
49 | break;
50 | case ProxyActionKind.Stopping:
51 | setProxyState('loading');
52 | break;
53 | case ProxyActionKind.Stopped:
54 | setProxyState('off');
55 | break;
56 | case ProxyActionKind.StopError:
57 | AppToaster.show({
58 | message: t('startStopButton.stopError', { error: action.error }),
59 | intent: 'danger',
60 | });
61 | setProxyState('off');
62 | break;
63 | case ProxyActionKind.UnsupportedDE:
64 | AppToaster.show({
65 | message: (
66 |
67 | ,
71 | br:
,
72 | }}
73 | />
74 |
75 | ),
76 | intent: 'warning',
77 | });
78 | break;
79 |
80 | default:
81 | console.log('unknown proxy action', action);
82 | }
83 | });
84 |
85 | return cancel;
86 | }, [t]);
87 |
88 | return (
89 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/internal/app/wndproc_windows.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "context"
5 | "syscall"
6 | "unsafe"
7 |
8 | "github.com/wailsapp/wails/v2/pkg/runtime"
9 | "golang.org/x/sys/windows"
10 | )
11 |
12 | const (
13 | // GWLP_WNDPROC is used with GetWindowLongPtrW and SetWindowLongPtrW to retrieve and overwrite a window's WndProc.
14 | // Its value, -4 in two's complement, is defined here explicitly as a uintptr to avoid compiler overflow warnings
15 | // when converting to an unsigned type.
16 | // This is safe as long as we only target 64-bit architectures.
17 | GWLP_WNDPROC = uintptr(0xFFFFFFFFFFFFFFFC)
18 |
19 | // WM_ENDSESSION message informs the application about a session ending.
20 | //
21 | // For more message number identifiers, see https://gitlab.winehq.org/wine/wine/-/wikis/Wine-Developer's-Guide/List-of-Windows-Messages.
22 | WM_ENDSESSION = 0x0016
23 | ENDSESSION_CLOSEAPP = 0x1
24 | )
25 |
26 | var (
27 | modUser32 = windows.NewLazySystemDLL("user32.dll")
28 |
29 | procEnumWindows = modUser32.NewProc("EnumWindows")
30 | procGetWindowThreadProcessId = modUser32.NewProc("GetWindowThreadProcessId")
31 | procGetWindowLongPtrW = modUser32.NewProc("GetWindowLongPtrW")
32 | procSetWindowLongPtrW = modUser32.NewProc("SetWindowLongPtrW")
33 | procCallWindowProcW = modUser32.NewProc("CallWindowProcW")
34 | )
35 |
36 | func runShutdownOnWmEndsession(ctx context.Context) {
37 | processId := windows.GetCurrentProcessId()
38 | windowHandle := findWindowByProcessId(processId)
39 | originalWndProc := getWindowProcPointer(windowHandle)
40 |
41 | newWndProc := func(hwnd windows.Handle, msg uint32, wParam, lParam uintptr) uintptr {
42 | // lParam: ENDSESSION_CLOSEAPP && wParam: FALSE identifies a condition where the application should not shut down:
43 | // https://learn.microsoft.com/en-us/windows/win32/shutdown/wm-endsession#parameters
44 | if msg == WM_ENDSESSION && !(lParam == ENDSESSION_CLOSEAPP && wParam == 0) {
45 | runtime.Quit(ctx)
46 | // https://learn.microsoft.com/en-us/windows/win32/shutdown/wm-endsession#return-value
47 | return 0
48 | }
49 |
50 | // Let Wails's WndProc handle other messages.
51 | return callWindowProc(originalWndProc, hwnd, msg, wParam, lParam)
52 | }
53 |
54 | subclassWndProc(windowHandle, newWndProc)
55 | }
56 |
57 | func findWindowByProcessId(processId uint32) windows.Handle {
58 | var targetHwnd windows.Handle
59 | cb := func(hwnd windows.Handle, _ uintptr) uintptr {
60 | wndProcessId := getWindowProcessId(hwnd)
61 | if wndProcessId == processId {
62 | targetHwnd = hwnd
63 | return 0
64 | }
65 | return 1
66 | }
67 | procEnumWindows.Call(syscall.NewCallback(cb), 0)
68 | return targetHwnd
69 | }
70 |
71 | func getWindowProcPointer(hwnd windows.Handle) uintptr {
72 | wndProc, _, _ := procGetWindowLongPtrW.Call(uintptr(hwnd), GWLP_WNDPROC)
73 | return wndProc
74 | }
75 |
76 | func getWindowProcessId(hwnd windows.Handle) uint32 {
77 | var processId uint32
78 | procGetWindowThreadProcessId.Call(
79 | uintptr(hwnd),
80 | uintptr(unsafe.Pointer(&processId)),
81 | )
82 | return processId
83 | }
84 |
85 | func callWindowProc(lpPrevWndFunc uintptr, hwnd windows.Handle, msg uint32, wParam, lParam uintptr) uintptr {
86 | ret, _, _ := procCallWindowProcW.Call(
87 | lpPrevWndFunc,
88 | uintptr(hwnd),
89 | uintptr(msg),
90 | wParam,
91 | lParam,
92 | )
93 | return ret
94 | }
95 |
96 | func subclassWndProc(hwnd windows.Handle, fn any) {
97 | procSetWindowLongPtrW.Call(
98 | uintptr(hwnd),
99 | GWLP_WNDPROC,
100 | syscall.NewCallback(fn),
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/.github/workflows/scorecard.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub. They are provided
2 | # by a third-party and are governed by separate terms of service, privacy
3 | # policy, and support documentation.
4 |
5 | name: Scorecard supply-chain security
6 | on:
7 | # For Branch-Protection check. Only the default branch is supported. See
8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
9 | branch_protection_rule:
10 | # To guarantee Maintained check is occasionally updated. See
11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
12 | schedule:
13 | - cron: '31 15 * * 2'
14 | push:
15 | branches: [ "master" ]
16 |
17 | # Declare default permissions as read only.
18 | permissions: read-all
19 |
20 | jobs:
21 | analysis:
22 | name: Scorecard analysis
23 | runs-on: ubuntu-latest
24 | # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
25 | if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
26 | permissions:
27 | # Needed to upload the results to code-scanning dashboard.
28 | security-events: write
29 | # Needed to publish results and get a badge (see publish_results below).
30 | id-token: write
31 | # Uncomment the permissions below if installing in a private repository.
32 | # contents: read
33 | # actions: read
34 |
35 | steps:
36 | - name: "Checkout code"
37 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
38 | with:
39 | persist-credentials: false
40 |
41 | - name: "Run analysis"
42 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
43 | with:
44 | results_file: results.sarif
45 | results_format: sarif
46 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
47 | # - you want to enable the Branch-Protection check on a *public* repository, or
48 | # - you are installing Scorecard on a *private* repository
49 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
50 | # repo_token: ${{ secrets.SCORECARD_TOKEN }}
51 |
52 | # Public repositories:
53 | # - Publish results to OpenSSF REST API for easy access by consumers
54 | # - Allows the repository to include the Scorecard badge.
55 | # - See https://github.com/ossf/scorecard-action#publishing-results.
56 | # For private repositories:
57 | # - `publish_results` will always be set to `false`, regardless
58 | # of the value entered here.
59 | publish_results: true
60 |
61 | # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
62 | # file_mode: git
63 |
64 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
65 | # format to the repository Actions tab.
66 | - name: "Upload artifact"
67 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
68 | with:
69 | name: SARIF file
70 | path: results.sarif
71 | retention-days: 5
72 |
73 | # Upload the results to GitHub's code scanning dashboard (optional).
74 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard
75 | - name: "Upload to code-scanning"
76 | uses: github/codeql-action/upload-sarif@v3
77 | with:
78 | sarif_file: results.sarif
79 |
--------------------------------------------------------------------------------
/frontend/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { initReactI18next } from 'react-i18next';
3 |
4 | import { GetLocale, SetLocale } from '../../wailsjs/go/cfg/Config';
5 |
6 | import deDE from './locales/de-DE.json';
7 | import enUS from './locales/en-US.json';
8 | import frFR from './locales/fr-FR.json';
9 | import itIT from './locales/it-IT.json';
10 | import kkKZ from './locales/kk-KZ.json';
11 | import ruRU from './locales/ru-RU.json';
12 | import zhCN from './locales/zh-CN.json';
13 | import zhTW from './locales/zh-TW.json';
14 |
15 | export const SUPPORTED_LOCALES = [
16 | 'en',
17 | 'en-US',
18 | 'de',
19 | 'de-DE',
20 | 'it',
21 | 'it-IT',
22 | 'kk',
23 | 'kk-KZ',
24 | 'ru',
25 | 'ru-RU',
26 | 'zh',
27 | 'zh-CN',
28 | 'zh-TW',
29 | 'fr',
30 | 'fr-FR',
31 | ] as const;
32 | export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
33 | export const FALLBACK_LOCALE: SupportedLocale = 'en-US';
34 |
35 | export interface LocaleItem {
36 | value: SupportedLocale;
37 | label: string;
38 | }
39 |
40 | export const LOCALE_LABELS: LocaleItem[] = [
41 | { value: 'en-US', label: 'English' },
42 | { value: 'de-DE', label: 'Deutsch' },
43 | { value: 'kk-KZ', label: 'Қазақша' },
44 | { value: 'ru-RU', label: 'Русский' },
45 | { value: 'zh-CN', label: '中文(简体)' },
46 | { value: 'zh-TW', label: '中文(繁體)' },
47 | { value: 'it-IT', label: 'Italiano' },
48 | { value: 'fr-FR', label: 'Français' },
49 | ];
50 |
51 | // Sort language options into a consistent, user-friendly alphabetical order.
52 | // Uses Unicode root collation ("und") to keep the order stable and identical for everyone.
53 | const localeLabelsCollator = new Intl.Collator('und', {
54 | usage: 'sort',
55 | sensitivity: 'base',
56 | });
57 | LOCALE_LABELS.sort((a, b) => localeLabelsCollator.compare(a.label, b.label));
58 |
59 | export function detectSystemLocale(): SupportedLocale {
60 | const browserLang = navigator.language;
61 | const detected = SUPPORTED_LOCALES.includes(browserLang as any) ? (browserLang as SupportedLocale) : FALLBACK_LOCALE;
62 |
63 | return detected;
64 | }
65 |
66 | export function getCurrentLocale(): SupportedLocale {
67 | return (i18n.language as SupportedLocale) || FALLBACK_LOCALE;
68 | }
69 |
70 | export async function changeLocale(locale: SupportedLocale) {
71 | const normalized = SUPPORTED_LOCALES.includes(locale) ? locale : FALLBACK_LOCALE;
72 | await i18n.changeLanguage(normalized);
73 | await SetLocale(normalized);
74 | }
75 |
76 | export async function initI18n() {
77 | let locale = await GetLocale();
78 | if (locale === '') {
79 | const detected = detectSystemLocale();
80 | await SetLocale(detected);
81 | locale = detected;
82 | }
83 |
84 | return i18n.use(initReactI18next).init({
85 | resources: {
86 | en: { translation: enUS },
87 | 'en-US': { translation: enUS },
88 | de: { translation: deDE },
89 | 'de-DE': { translation: deDE },
90 | kk: { translation: kkKZ },
91 | 'kk-KZ': { translation: kkKZ },
92 | ru: { translation: ruRU },
93 | 'ru-RU': { translation: ruRU },
94 | zh: { translation: zhCN },
95 | 'zh-CN': { translation: zhCN },
96 | 'zh-TW': { translation: zhTW },
97 | it: { translation: itIT },
98 | 'it-IT': { translation: itIT },
99 | fr: { translation: frFR },
100 | 'fr-FR': { translation: frFR },
101 | },
102 | lng: locale,
103 | fallbackLng: FALLBACK_LOCALE,
104 | returnNull: false,
105 | returnEmptyString: false,
106 | interpolation: {
107 | escapeValue: false,
108 | },
109 | react: {
110 | useSuspense: false,
111 | },
112 | });
113 | }
114 |
--------------------------------------------------------------------------------
/internal/sysproxy/manager.go:
--------------------------------------------------------------------------------
1 | // Package sysproxy implements [Manager], providing a unified, cross-platform interface for configuring system proxies.
2 | //
3 | // sysproxy uses PAC (Proxy Auto-Config) as the configuration method due to the extensive use of proxy exceptions.
4 | // While declarative configuration methods also support exceptions, they often impose strict limits on the number
5 | // of characters that can be specified. For example, the ProxyOverride registry key on Windows is limited to
6 | // approximately 2000 characters, and the equivalent setting on macOS has a limit of around 650 characters.
7 | // In contrast, PAC files can typically be up to 1MB in size, which is more than sufficient for our use case.
8 | //
9 | // To discover more about PAC, see:
10 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file
11 | package sysproxy
12 |
13 | import (
14 | "errors"
15 | "fmt"
16 | "log"
17 | "net"
18 | "net/http"
19 | "time"
20 | )
21 |
22 | var ErrUnsupportedDesktopEnvironment = errors.New("system proxy configuration is currently only supported on GNOME and KDE")
23 |
24 | type Manager struct {
25 | pacPort int
26 | server *http.Server
27 | }
28 |
29 | // NewManager creates a new system proxy Manager.
30 | // The PAC server will listen on the given pacPort.
31 | // If pacPort is 0, a random port will be chosen.
32 | func NewManager(pacPort int) *Manager {
33 | return &Manager{
34 | pacPort: pacPort,
35 | }
36 | }
37 |
38 | // Set configures the system proxy to use the proxy server listening on the given port.
39 | func (m *Manager) Set(proxyPort int, userConfiguredExcludedHosts []string) error {
40 | pac := renderPac(proxyPort, userConfiguredExcludedHosts)
41 |
42 | actualPort, err := m.makeServer(pac)
43 | if err != nil {
44 | return fmt.Errorf("make server: %v", err)
45 | }
46 |
47 | pacURL := fmt.Sprintf("http://127.0.0.1:%d/proxy.pac", actualPort)
48 | if err := setSystemProxy(pacURL); err != nil {
49 | return fmt.Errorf("set system proxy with URL %q: %w", pacURL, err)
50 | }
51 |
52 | return nil
53 | }
54 |
55 | // Clear removes the system proxy configuration.
56 | func (m *Manager) Clear() error {
57 | if m.server == nil {
58 | log.Println("warning: trying to clear system proxy without setting it first")
59 | return nil
60 | }
61 |
62 | if err := unsetSystemProxy(); err != nil {
63 | return fmt.Errorf("unset system proxy: %v", err)
64 | }
65 |
66 | if err := m.server.Close(); err != nil {
67 | return fmt.Errorf("close: %v", err)
68 | }
69 | m.server = nil
70 | return nil
71 | }
72 |
73 | // makeServer starts an HTTP server that serves the PAC file.
74 | // It returns the actual port the server is listening on, which may be different from the requested port if the latter is 0.
75 | func (m *Manager) makeServer(pac []byte) (int, error) {
76 | mux := http.NewServeMux()
77 | mux.HandleFunc("/proxy.pac", func(w http.ResponseWriter, _ *http.Request) {
78 | w.Header().Set("Content-Type", "application/x-ns-proxy-autoconfig")
79 | w.WriteHeader(http.StatusOK)
80 | w.Write(pac)
81 | })
82 |
83 | m.server = &http.Server{
84 | Handler: mux,
85 | ReadTimeout: time.Minute,
86 | WriteTimeout: time.Minute,
87 | }
88 | listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", m.pacPort))
89 | if err != nil {
90 | return -1, fmt.Errorf("listen: %v", err)
91 | }
92 | actualPort := listener.Addr().(*net.TCPAddr).Port
93 | log.Printf("PAC server listening on port %d", actualPort)
94 |
95 | go func() {
96 | if err := m.server.Serve(listener); err != nil && err != http.ErrServerClosed {
97 | log.Printf("error serving PAC: %v", err)
98 | }
99 | }()
100 |
101 | return actualPort, nil
102 | }
103 |
--------------------------------------------------------------------------------
/frontend/src/Intro/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonGroup, ProgressBar } from '@blueprintjs/core';
2 | import { useEffect, useState } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import './index.css';
6 | import { GetFilterListsByLocales } from '../../wailsjs/go/cfg/Config';
7 | import { filter } from '../../wailsjs/go/models';
8 | import { useProxyState } from '../context/ProxyStateContext';
9 | import { StartStopButton } from '../StartStopButton';
10 |
11 | import { ConnectScreen } from './ConnectScreen';
12 | import { FilterListsScreen } from './FilterListsScreen';
13 | import { SettingsScreen } from './SettingsScreen';
14 | import { WelcomeScreen } from './WelcomeScreen';
15 |
16 | interface IntroProps {
17 | onClose: () => void;
18 | }
19 |
20 | export function Intro({ onClose }: IntroProps) {
21 | const { t } = useTranslation();
22 |
23 | const [currentScreen, setCurrentScreen] = useState(1);
24 | const [filterLists, setFilterLists] = useState([]);
25 | const [filterListsLoading, setFilterListsLoading] = useState(true);
26 |
27 | useEffect(() => {
28 | GetFilterListsByLocales(navigator.languages as string[])
29 | .then((filterLists) => {
30 | if (filterLists) setFilterLists(filterLists);
31 | setFilterListsLoading(false);
32 | })
33 | .catch((ex) => {
34 | console.error(ex);
35 | setFilterListsLoading(false);
36 | });
37 | }, []);
38 |
39 | const { proxyState } = useProxyState();
40 |
41 | const totalScreens = filterLists.length > 0 ? 4 : 3;
42 |
43 | useEffect(() => {
44 | if (currentScreen === totalScreens && proxyState === 'on') {
45 | onClose();
46 | }
47 | }, [proxyState, currentScreen, totalScreens, onClose]);
48 |
49 | const renderCurrentScreen = () => {
50 | if (filterLists.length > 0) {
51 | switch (currentScreen) {
52 | case 1:
53 | return ;
54 | case 2:
55 | return ;
56 | case 3:
57 | return ;
58 | case 4:
59 | return ;
60 | default:
61 | return null;
62 | }
63 | }
64 |
65 | switch (currentScreen) {
66 | case 1:
67 | return ;
68 | case 2:
69 | return ;
70 | case 3:
71 | return ;
72 | default:
73 | return null;
74 | }
75 | };
76 |
77 | return (
78 | <>
79 | {renderCurrentScreen()}
80 |
81 | {currentScreen < totalScreens ? (
82 | <>
83 |
90 |
91 |
92 |
95 |
106 |
107 | >
108 | ) : (
109 |
110 | )}
111 |
112 | >
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/cfg/Config.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 AddFilterList(arg1) {
6 | return window['go']['cfg']['Config']['AddFilterList'](arg1);
7 | }
8 |
9 | export function AddFilterLists(arg1) {
10 | return window['go']['cfg']['Config']['AddFilterLists'](arg1);
11 | }
12 |
13 | export function ExportDebugData() {
14 | return window['go']['cfg']['Config']['ExportDebugData']();
15 | }
16 |
17 | export function GetCAInstalled() {
18 | return window['go']['cfg']['Config']['GetCAInstalled']();
19 | }
20 |
21 | export function GetFilterLists() {
22 | return window['go']['cfg']['Config']['GetFilterLists']();
23 | }
24 |
25 | export function GetFilterListsByLocales(arg1) {
26 | return window['go']['cfg']['Config']['GetFilterListsByLocales'](arg1);
27 | }
28 |
29 | export function GetFirstLaunch() {
30 | return window['go']['cfg']['Config']['GetFirstLaunch']();
31 | }
32 |
33 | export function GetIgnoredHosts() {
34 | return window['go']['cfg']['Config']['GetIgnoredHosts']();
35 | }
36 |
37 | export function GetLocale() {
38 | return window['go']['cfg']['Config']['GetLocale']();
39 | }
40 |
41 | export function GetPACPort() {
42 | return window['go']['cfg']['Config']['GetPACPort']();
43 | }
44 |
45 | export function GetPort() {
46 | return window['go']['cfg']['Config']['GetPort']();
47 | }
48 |
49 | export function GetRules() {
50 | return window['go']['cfg']['Config']['GetRules']();
51 | }
52 |
53 | export function GetTargetTypeFilterLists(arg1) {
54 | return window['go']['cfg']['Config']['GetTargetTypeFilterLists'](arg1);
55 | }
56 |
57 | export function GetUpdatePolicy() {
58 | return window['go']['cfg']['Config']['GetUpdatePolicy']();
59 | }
60 |
61 | export function GetVersion() {
62 | return window['go']['cfg']['Config']['GetVersion']();
63 | }
64 |
65 | export function Lock() {
66 | return window['go']['cfg']['Config']['Lock']();
67 | }
68 |
69 | export function RLock() {
70 | return window['go']['cfg']['Config']['RLock']();
71 | }
72 |
73 | export function RLocker() {
74 | return window['go']['cfg']['Config']['RLocker']();
75 | }
76 |
77 | export function RUnlock() {
78 | return window['go']['cfg']['Config']['RUnlock']();
79 | }
80 |
81 | export function RemoveFilterList(arg1) {
82 | return window['go']['cfg']['Config']['RemoveFilterList'](arg1);
83 | }
84 |
85 | export function RunMigrations() {
86 | return window['go']['cfg']['Config']['RunMigrations']();
87 | }
88 |
89 | export function Save() {
90 | return window['go']['cfg']['Config']['Save']();
91 | }
92 |
93 | export function SetCAInstalled(arg1) {
94 | return window['go']['cfg']['Config']['SetCAInstalled'](arg1);
95 | }
96 |
97 | export function SetIgnoredHosts(arg1) {
98 | return window['go']['cfg']['Config']['SetIgnoredHosts'](arg1);
99 | }
100 |
101 | export function SetLocale(arg1) {
102 | return window['go']['cfg']['Config']['SetLocale'](arg1);
103 | }
104 |
105 | export function SetPort(arg1) {
106 | return window['go']['cfg']['Config']['SetPort'](arg1);
107 | }
108 |
109 | export function SetRules(arg1) {
110 | return window['go']['cfg']['Config']['SetRules'](arg1);
111 | }
112 |
113 | export function SetUpdatePolicy(arg1) {
114 | return window['go']['cfg']['Config']['SetUpdatePolicy'](arg1);
115 | }
116 |
117 | export function ToggleFilterList(arg1, arg2) {
118 | return window['go']['cfg']['Config']['ToggleFilterList'](arg1, arg2);
119 | }
120 |
121 | export function TryLock() {
122 | return window['go']['cfg']['Config']['TryLock']();
123 | }
124 |
125 | export function TryRLock() {
126 | return window['go']['cfg']['Config']['TryRLock']();
127 | }
128 |
129 | export function Unlock() {
130 | return window['go']['cfg']['Config']['Unlock']();
131 | }
132 |
--------------------------------------------------------------------------------
/frontend/src/SettingsManager/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Tag } from '@blueprintjs/core';
2 | import { useEffect, useState } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import './index.css';
6 |
7 | import { IsNoSelfUpdate } from '../../wailsjs/go/app/App';
8 | import { GetVersion } from '../../wailsjs/go/cfg/Config';
9 | import { BrowserOpenURL } from '../../wailsjs/runtime';
10 | import { BrowserLink } from '../common/BrowserLink';
11 | import { useProxyState } from '../context/ProxyStateContext';
12 |
13 | import { AutostartSwitch } from './AutostartSwitch';
14 | import { AutoupdateSwitch } from './AutoupdateSwitch';
15 | import { ExportDebugDataButton } from './ExportDebugDataButton';
16 | import { ExportLogsButton } from './ExportLogsButton';
17 | import { IgnoredHostsInput } from './IgnoredHostsInput';
18 | import { LocaleSelector } from './LocaleSelector';
19 | import { PortInput } from './PortInput';
20 | import { ThemeSelector } from './ThemeSelector';
21 | import { UninstallCADialog } from './UninstallCADialog';
22 |
23 | const GITHUB_URL = 'https://github.com/ZenPrivacy/zen-desktop';
24 | const CHANGELOG_URL = `${GITHUB_URL}/blob/master/CHANGELOG.md`;
25 |
26 | export function SettingsManager() {
27 | const { t } = useTranslation();
28 | const [state, setState] = useState({
29 | version: '',
30 | updatePolicy: '',
31 | showUpdateRadio: false,
32 | });
33 | const { proxyState } = useProxyState();
34 |
35 | useEffect(() => {
36 | (async () => {
37 | const [version, noSelfUpdate] = await Promise.all([GetVersion(), IsNoSelfUpdate()]);
38 |
39 | setState((prev) => ({
40 | ...prev,
41 | showUpdateRadio: !noSelfUpdate,
42 | version,
43 | }));
44 | })();
45 | }, []);
46 |
47 | return (
48 |
49 |
50 |
51 | {t('settings.sections.app')}
52 |
53 |
54 |
55 |
56 |
57 | {state.showUpdateRadio &&
}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {t('settings.sections.advanced')}
69 |
70 |
71 |
76 |
77 |
78 |
79 |
80 | Zen
81 |
82 |
{t('settings.about.tagline')}
83 |
84 | {t('settings.about.version')}: {state.version}
85 |
86 | ({t('settings.about.changelog')})
87 |
88 |
89 |
© 2025 Zen Privacy Project Developers
90 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/internal/autostart/autostart_linux.go:
--------------------------------------------------------------------------------
1 | // References:
2 | // - https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html
3 |
4 | package autostart
5 |
6 | import (
7 | "fmt"
8 | "log"
9 | "os"
10 | "path/filepath"
11 | "text/template"
12 |
13 | "github.com/ZenPrivacy/zen-desktop/internal/constants"
14 | )
15 |
16 | const (
17 | desktopTemplate = `[Desktop Entry]
18 | Name={{.Name}}
19 | Comment=Automatically start {{.Name}} at user login
20 | Type=Application
21 | Exec={{.ExecPath}} --start --hidden
22 | X-GNOME-Autostart-enabled=true`
23 | )
24 |
25 | type desktopTemplateParameters struct {
26 | Name string
27 | ExecPath string
28 | }
29 |
30 | func (m Manager) IsEnabled() (enabled bool, err error) {
31 | defer func() {
32 | if err != nil {
33 | log.Printf("error checking registry key: %s", err)
34 | }
35 | }()
36 |
37 | path, err := getDesktopPath()
38 | if err != nil {
39 | return false, fmt.Errorf("get desktop path: %w", err)
40 | }
41 |
42 | _, err = os.Stat(path)
43 | return err == nil, nil
44 | }
45 |
46 | func (m Manager) Enable() (err error) {
47 | defer func() {
48 | if err != nil {
49 | log.Printf("error enabling autostart: %s", err)
50 | }
51 | }()
52 |
53 | if enabled, err := m.IsEnabled(); err != nil {
54 | return fmt.Errorf("check enabled: %w", err)
55 | } else if enabled {
56 | return nil
57 | }
58 |
59 | execPath, err := getExecPath()
60 | if err != nil {
61 | return fmt.Errorf("get exec path: %w", err)
62 | }
63 | autostartDir, err := getAutostartDir()
64 | if err != nil {
65 | return fmt.Errorf("get autostart dir: %w", err)
66 | }
67 | desktopPath, err := getDesktopPath()
68 | if err != nil {
69 | return fmt.Errorf("get .desktop path: %w", err)
70 | }
71 |
72 | if err := os.MkdirAll(autostartDir, 0755); err != nil {
73 | return fmt.Errorf("create autostart dir: %w", err)
74 | }
75 | f, err := os.Create(desktopPath)
76 | if err != nil {
77 | return fmt.Errorf("create .desktop file: %w", err)
78 | }
79 | defer f.Close()
80 |
81 | t := template.Must(template.New("desktop").Parse(desktopTemplate))
82 |
83 | if err := t.Execute(f, desktopTemplateParameters{
84 | Name: constants.AppName,
85 | ExecPath: execPath,
86 | }); err != nil {
87 | return fmt.Errorf("execute template: %w", err)
88 | }
89 |
90 | return nil
91 | }
92 |
93 | func (m Manager) Disable() (err error) {
94 | defer func() {
95 | if err != nil {
96 | log.Printf("error disabling autostart: %s", err)
97 | }
98 | }()
99 |
100 | if enabled, err := m.IsEnabled(); err != nil {
101 | return fmt.Errorf("check enabled: %w", err)
102 | } else if !enabled {
103 | return nil
104 | }
105 |
106 | desktopPath, err := getDesktopPath()
107 | if err != nil {
108 | return fmt.Errorf("get .desktop path: %w", err)
109 | }
110 | if err := os.Remove(desktopPath); err != nil {
111 | return fmt.Errorf("remove .desktop file: %w", err)
112 | }
113 |
114 | return nil
115 | }
116 |
117 | // getAutostartDir returns the autostart directory as defined in:
118 | // https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html
119 | func getAutostartDir() (string, error) {
120 | if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
121 | return configHome, nil
122 | }
123 |
124 | homeDir, err := os.UserHomeDir()
125 | if err != nil {
126 | return "", fmt.Errorf("get user home dir: %w", err)
127 | }
128 | return filepath.Join(homeDir, ".config"), nil
129 | }
130 |
131 | // getDesktopPath returns the path of the .desktop file that autostarts the app.
132 | func getDesktopPath() (string, error) {
133 | folder, err := getAutostartDir()
134 | if err != nil {
135 | return "", fmt.Errorf("get desktop folder: %w", err)
136 | }
137 |
138 | return filepath.Join(folder, constants.AppName+"-autostart.desktop"), nil
139 | }
140 |
--------------------------------------------------------------------------------
/internal/selfupdate/selfupdate_test.go:
--------------------------------------------------------------------------------
1 | package selfupdate
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "os"
7 | "path/filepath"
8 | "testing"
9 | )
10 |
11 | func TestIsNewer(t *testing.T) {
12 | t.Parallel()
13 |
14 | t.Run("fails on invalid current version", func(t *testing.T) {
15 | t.Parallel()
16 | su := SelfUpdater{
17 | version: "v100",
18 | }
19 | if _, err := su.isNewer("v1.0.0"); err == nil {
20 | t.Error("got nil, want error")
21 | }
22 | })
23 |
24 | t.Run("fails on invalid new version", func(t *testing.T) {
25 | t.Parallel()
26 | su := SelfUpdater{
27 | version: "v1.0.0",
28 | }
29 | if _, err := su.isNewer("v100"); err == nil {
30 | t.Error("got nil, want error")
31 | }
32 | })
33 |
34 | t.Run("returns true if new version has larger patch version than current", func(t *testing.T) {
35 | t.Parallel()
36 | su := SelfUpdater{
37 | version: "v3.2.2",
38 | }
39 | newer, err := su.isNewer("v3.2.3")
40 | if err != nil {
41 | t.Error("got error, want nil")
42 | }
43 | if !newer {
44 | t.Error("got false, want true")
45 | }
46 | })
47 |
48 | t.Run("returns true if new version has larger minor version than current", func(t *testing.T) {
49 | t.Parallel()
50 | su := SelfUpdater{
51 | version: "v0.1.0",
52 | }
53 | newer, err := su.isNewer("v0.5.0")
54 | if err != nil {
55 | t.Error("got error, want nil")
56 | }
57 | if !newer {
58 | t.Error("got false, want true")
59 | }
60 | })
61 |
62 | t.Run("returns true if new version has larger major version than current", func(t *testing.T) {
63 | t.Parallel()
64 | su := SelfUpdater{
65 | version: "v10.0.0",
66 | }
67 | newer, err := su.isNewer("v14.2.8")
68 | if err != nil {
69 | t.Error("got error, want nil")
70 | }
71 | if !newer {
72 | t.Error("got false, want true")
73 | }
74 | })
75 |
76 | t.Run("returns false if versions are equal", func(t *testing.T) {
77 | t.Parallel()
78 | su := SelfUpdater{
79 | version: "v4.1.1",
80 | }
81 | newer, err := su.isNewer("v4.1.1")
82 | if err != nil {
83 | t.Error("got error, want nil")
84 | }
85 | if newer {
86 | t.Error("got true, want false")
87 | }
88 | })
89 |
90 | t.Run("returns false if new version is older than current", func(t *testing.T) {
91 | t.Parallel()
92 | su := SelfUpdater{
93 | version: "v1.0.0",
94 | }
95 | newer, err := su.isNewer("v0.9.9")
96 | if err != nil {
97 | t.Error("got error, want nil")
98 | }
99 | if newer {
100 | t.Error("got true, want false")
101 | }
102 | })
103 | }
104 |
105 | func TestPathTraversal(t *testing.T) {
106 |
107 | t.Run("blocks traversal attack", func(t *testing.T) {
108 | var buf bytes.Buffer
109 | zw := zip.NewWriter(&buf)
110 |
111 | f, err := zw.Create("../../evil.txt")
112 | if err != nil {
113 | t.Fatalf("failed to create zip entry: %v", err)
114 | }
115 |
116 | _, err = f.Write([]byte("should not escape"))
117 | if err != nil {
118 | t.Fatalf("failed to write to zip: %v", err)
119 | }
120 |
121 | if err := zw.Close(); err != nil {
122 | t.Fatalf("failed to close zip writer: %v", err)
123 | }
124 |
125 | tmpZip := filepath.Join(t.TempDir(), "malicious.zip")
126 | err = os.WriteFile(tmpZip, buf.Bytes(), 0644)
127 | if err != nil {
128 | t.Fatalf("failed to write zip file: %v", err)
129 | }
130 |
131 | parentDir := t.TempDir()
132 | dest := filepath.Join(parentDir, "extract")
133 |
134 | err = unzip(tmpZip, dest)
135 | if err == nil {
136 | t.Fatal("expected error due to path traversal, got nil")
137 | }
138 |
139 | entries, err := os.ReadDir(parentDir)
140 | if err != nil {
141 | t.Fatalf("failed to read parent dir: %v", err)
142 | }
143 |
144 | for _, entry := range entries {
145 | if entry.Name() == "extract" {
146 | continue
147 | }
148 | t.Fatalf("unexpected file created outside destination: %s", entry.Name())
149 | }
150 | })
151 | }
152 |
--------------------------------------------------------------------------------
/tasks/build/Taskfile-darwin.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. Only recommended for use in CI/CD pipelines.
11 | cmds:
12 | - task: app
13 | - task: codesign
14 | vars:
15 | FILENAME: build/bin/Zen.app
16 | - task: notarize
17 | vars:
18 | FILENAME: build/bin/Zen.app
19 | - task: dmg
20 | vars:
21 | APP_FILENAME: build/bin/Zen.app
22 | - task: codesign
23 | vars:
24 | FILENAME: build/bin/Zen.dmg
25 | - task: notarize
26 | vars:
27 | FILENAME: build/bin/Zen.dmg
28 |
29 | prod-noupdate:
30 | desc: Create a production build of the application with self-updates disabled. Only recommended for use in CI/CD pipelines. Doesn't include a DMG installer.
31 | cmds:
32 | - task: app-noupdate
33 | - task: codesign
34 | vars:
35 | FILENAME: build/bin/Zen.app
36 | - task: notarize
37 | vars:
38 | FILENAME: build/bin/Zen.app
39 |
40 | deps:
41 | desc: Install the dependencies required to create a production build.
42 | cmds:
43 | - npm i -g create-dmg@^7.0.0
44 |
45 | app:
46 | desc: Build the .app application bundle. The .app file will be placed in the build/bin directory.
47 | cmds:
48 | - wails build -platform "darwin/{{default .ARCH64 .ARCH}}" -m -skipbindings -ldflags "-X 'github.com/ZenPrivacy/zen-desktop/internal/cfg.Version={{.GIT_TAG}}'" -tags prod
49 |
50 | app-noupdate:
51 | desc: Build the .app application bundle with self-updates disabled. The .app file will be placed in the build/bin directory.
52 | cmds:
53 | - wails build -platform "darwin/{{default .ARCH64 .ARCH}}" -m -skipbindings -ldflags "-X 'github.com/ZenPrivacy/zen-desktop/internal/cfg.Version={{.GIT_TAG}}' -X 'github.com/ZenPrivacy/zen-desktop/internal/selfupdate.NoSelfUpdate=true'" -tags prod
54 |
55 | dmg:
56 | desc: Create a DMG installer for the application. The DMG file will be placed in the build/bin directory.
57 | cmds:
58 | - create-dmg "{{default .DEFAULT_APP_FILENAME .APP_FILENAME}}" --overwrite
59 | - mv Zen*.dmg build/bin/Zen.dmg # create-dmg creates the disk image in the current directory
60 | - echo "Built Zen.dmg"
61 | vars:
62 | DEFAULT_APP_FILENAME: build/bin/Zen.app
63 |
64 | setup-keychain:
65 | desc: Set up the keychain profile for signing the application.
66 | cmds:
67 | - security create-keychain -p "$CI_KEYCHAIN_PWD" zen.keychain
68 | - security default-keychain -s zen.keychain
69 | - security unlock-keychain -p "$CI_KEYCHAIN_PWD" zen.keychain
70 | - echo "$CERTIFICATE" | base64 --decode > certificate.p12
71 | - security import certificate.p12 -k zen.keychain -P "$CERTIFICATE_PWD" -T /usr/bin/codesign
72 | - rm certificate.p12
73 | - security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$CI_KEYCHAIN_PWD" zen.keychain
74 | requires:
75 | vars: [CI_KEYCHAIN_PWD, CERTIFICATE, CERTIFICATE_PWD]
76 |
77 | codesign:
78 | desc: Codesign the specified file.
79 | internal: true
80 | cmds:
81 | - /usr/bin/codesign --force -s "$CERTIFICATE_NAME" --options runtime "{{.FILENAME}}" -v
82 | requires:
83 | vars: [FILENAME, CERTIFICATE_NAME]
84 |
85 | notarize:
86 | desc: Notarize the specified file.
87 | internal: true
88 | cmds:
89 | - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$NOTARIZATION_APPLE_ID" --team-id "$NOTARIZATION_TEAM_ID" --password "$NOTARIZATION_PWD"
90 | - ditto -c -k --keepParent "{{.FILENAME}}" notarization.zip
91 | - xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
92 | - xcrun stapler staple "{{.FILENAME}}"
93 | - rm notarization.zip
94 | requires:
95 | vars: [FILENAME, NOTARIZATION_APPLE_ID, NOTARIZATION_TEAM_ID, NOTARIZATION_PWD]
96 |
--------------------------------------------------------------------------------
/frontend/src/FilterLists/CreateFilterList/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Classes, FormGroup, InputGroup, Switch, Tooltip } from '@blueprintjs/core';
2 | import { InfoSign } from '@blueprintjs/icons';
3 | import { useRef, useState } from 'react';
4 | import { Trans, useTranslation } from 'react-i18next';
5 |
6 | import { AddFilterList } from '../../../wailsjs/go/cfg/Config';
7 | import { AppToaster } from '../../common/toaster';
8 | import { useProxyState } from '../../context/ProxyStateContext';
9 | import { FilterListType } from '../types';
10 | import './index.css';
11 |
12 | export function CreateFilterList({ onAdd }: { onAdd: () => void }) {
13 | const { t } = useTranslation();
14 | const { isProxyRunning } = useProxyState();
15 | const urlRef = useRef(null);
16 | const nameRef = useRef(null);
17 |
18 | const [trusted, setTrusted] = useState(false);
19 | const [loading, setLoading] = useState(false);
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
36 | ,
40 | strong: ,
41 | }}
42 | />
43 |
44 | }
45 | placement="top"
46 | minimal
47 | matchTargetWidth
48 | >
49 |
50 | {t('filterLists.trusted')}
51 |
52 |
53 |
54 | }
55 | labelFor="trusted"
56 | >
57 | {
62 | setTrusted(e.currentTarget.checked);
63 | }}
64 | />
65 |
66 |
67 |
68 |
106 |
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/internal/autostart/autostart_darwin.go:
--------------------------------------------------------------------------------
1 | // autostart_darwin.go provides autostart capabilities for macOS.
2 | // To add the app to autostart, it creates a launchd daemon definition under ~/Library/LaunchAgents.
3 | //
4 | // References:
5 | // - https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html
6 | // - man launchd.plist
7 |
8 | package autostart
9 |
10 | import (
11 | "fmt"
12 | "log"
13 | "os"
14 | "path/filepath"
15 | "text/template"
16 | )
17 |
18 | const (
19 | reverseDNSAppName = "net.zenprivacy.zen"
20 | // plistTemplate is a template for defining a launchd daemon.
21 | plistTemplate = `
22 |
23 |
24 |
25 | Label
26 | {{.ReverseDNSAppName}}
27 | Program
28 | {{.Program}}
29 | ProgramArguments
30 |
31 | {{.Program}}
32 | --start
33 |
34 | RunAtLoad
35 |
36 | AbandonProcessGroup
37 |
38 | ProcessType
39 | Interactive
40 |
41 | `
42 | )
43 |
44 | type plistTemplateParameters struct {
45 | Program string
46 | ReverseDNSAppName string
47 | }
48 |
49 | func (m Manager) IsEnabled() (enabled bool, err error) {
50 | defer func() {
51 | if err != nil {
52 | log.Printf("error checking registry key: %s", err)
53 | }
54 | }()
55 |
56 | plistPath, err := getPath()
57 | if err != nil {
58 | return false, fmt.Errorf("get launch plist path: %w", err)
59 | }
60 |
61 | _, err = os.Stat(plistPath)
62 | return err == nil, nil
63 | }
64 |
65 | func (m Manager) Enable() (err error) {
66 | defer func() {
67 | if err != nil {
68 | log.Printf("error enabling autostart: %s", err)
69 | }
70 | }()
71 |
72 | if enabled, err := m.IsEnabled(); err != nil {
73 | return fmt.Errorf("check enabled: %w", err)
74 | } else if enabled {
75 | return nil
76 | }
77 |
78 | execPath, err := getExecPath()
79 | if err != nil {
80 | return fmt.Errorf("get exec path: %w", err)
81 | }
82 |
83 | launchDir, err := getLaunchDir()
84 | if err != nil {
85 | return fmt.Errorf("get launch dir: %w", err)
86 | }
87 | plistPath, err := getPath()
88 | if err != nil {
89 | return fmt.Errorf("get launch plist path: %w", err)
90 | }
91 |
92 | if err := os.MkdirAll(launchDir, 0755); err != nil {
93 | return fmt.Errorf("create launch dir: %w", err)
94 | }
95 | f, err := os.Create(plistPath)
96 | if err != nil {
97 | return fmt.Errorf("create plist file: %w", err)
98 | }
99 | defer f.Close()
100 |
101 | t := template.Must(template.New("plist").Parse(plistTemplate))
102 |
103 | if err := t.Execute(f, plistTemplateParameters{
104 | ReverseDNSAppName: reverseDNSAppName,
105 | Program: execPath,
106 | }); err != nil {
107 | return fmt.Errorf("execute template: %w", err)
108 | }
109 |
110 | return nil
111 | }
112 |
113 | func (m Manager) Disable() (err error) {
114 | defer func() {
115 | if err != nil {
116 | log.Printf("error disabling autostart: %s", err)
117 | }
118 | }()
119 |
120 | if enabled, err := m.IsEnabled(); err != nil {
121 | return fmt.Errorf("check enabled: %w", err)
122 | } else if !enabled {
123 | return nil
124 | }
125 |
126 | plistPath, err := getPath()
127 | if err != nil {
128 | return fmt.Errorf("get launch plist path: %w", err)
129 | }
130 | if err := os.Remove(plistPath); err != nil {
131 | return fmt.Errorf("remove plist: %w", err)
132 | }
133 |
134 | return nil
135 | }
136 |
137 | func getPath() (string, error) {
138 | launchDir, err := getLaunchDir()
139 | if err != nil {
140 | return "", fmt.Errorf("get launch dir: %w", err)
141 | }
142 |
143 | return filepath.Join(launchDir, reverseDNSAppName+".plist"), nil
144 | }
145 |
146 | func getLaunchDir() (string, error) {
147 | homeDir, err := os.UserHomeDir()
148 | if err != nil {
149 | return "", fmt.Errorf("get user home dir: %w", err)
150 | }
151 |
152 | return filepath.Join(homeDir, "Library", "LaunchAgents"), nil
153 | }
154 |
--------------------------------------------------------------------------------
/internal/app/eventshandler.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/ZenPrivacy/zen-core/networkrules/rule"
8 | "github.com/wailsapp/wails/v2/pkg/runtime"
9 | )
10 |
11 | type eventsHandler struct {
12 | ctx context.Context
13 | }
14 |
15 | func newEventsHandler(ctx context.Context) *eventsHandler {
16 | return &eventsHandler{ctx: ctx}
17 | }
18 |
19 | type filterActionKind string
20 |
21 | const (
22 | filterChannel = "filter:action"
23 | filterActionBlock filterActionKind = "block"
24 | filterActionRedirect filterActionKind = "redirect"
25 | filterActionModify filterActionKind = "modify"
26 | )
27 |
28 | type filterAction struct {
29 | Kind filterActionKind `json:"kind"`
30 | Method string `json:"method"`
31 | URL string `json:"url"`
32 | To string `json:"to,omitempty"`
33 | Referer string `json:"referer,omitempty"`
34 | Rules []rule.Rule `json:"rules"`
35 | }
36 |
37 | type proxyState string
38 |
39 | // Only these states are handled via eventsHandler because the proxy can only start without any input from the user.
40 | const (
41 | proxyChannel = "proxy:action"
42 | proxyStarting proxyState = "starting"
43 | proxyStarted proxyState = "started"
44 | proxyStartError proxyState = "startError"
45 | proxyStopping proxyState = "stopping"
46 | proxyStopped proxyState = "stopped"
47 | proxyStopError proxyState = "stopError"
48 | unsupportedDE proxyState = "unsupportedDE"
49 | )
50 |
51 | type proxyAction struct {
52 | Kind proxyState `json:"kind"`
53 | Error string `json:"error"`
54 | }
55 |
56 | type updateActionKind string
57 |
58 | const (
59 | updateChannel = "app:update"
60 | updateAvailable updateActionKind = "updateAvailable"
61 | )
62 |
63 | type updateAction struct {
64 | Kind updateActionKind `json:"kind"`
65 | }
66 |
67 | func (e *eventsHandler) OnFilterBlock(method, url, referer string, rules []rule.Rule) {
68 | runtime.EventsEmit(e.ctx, filterChannel, filterAction{
69 | Kind: filterActionBlock,
70 | Method: method,
71 | URL: url,
72 | Referer: referer,
73 | Rules: rules,
74 | })
75 | }
76 |
77 | func (e *eventsHandler) OnFilterRedirect(method, url, to, referer string, rules []rule.Rule) {
78 | runtime.EventsEmit(e.ctx, filterChannel, filterAction{
79 | Kind: filterActionRedirect,
80 | Method: method,
81 | URL: url,
82 | To: to,
83 | Referer: referer,
84 | Rules: rules,
85 | })
86 | }
87 |
88 | func (e *eventsHandler) OnFilterModify(method, url, referer string, rules []rule.Rule) {
89 | runtime.EventsEmit(e.ctx, filterChannel, filterAction{
90 | Kind: filterActionModify,
91 | Method: method,
92 | URL: url,
93 | Referer: referer,
94 | Rules: rules,
95 | })
96 | }
97 |
98 | func (e *eventsHandler) OnProxyStarting() {
99 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
100 | Kind: proxyStarting,
101 | })
102 | }
103 |
104 | func (e *eventsHandler) OnProxyStarted() {
105 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
106 | Kind: proxyStarted,
107 | })
108 | }
109 |
110 | func (e *eventsHandler) OnProxyStartError(err error) {
111 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
112 | Kind: proxyStartError,
113 | Error: fmt.Sprint(err),
114 | })
115 | }
116 |
117 | func (e *eventsHandler) OnProxyStopping() {
118 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
119 | Kind: proxyStopping,
120 | })
121 | }
122 |
123 | func (e *eventsHandler) OnProxyStopped() {
124 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
125 | Kind: proxyStopped,
126 | })
127 | }
128 |
129 | func (e *eventsHandler) OnProxyStopError(err error) {
130 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
131 | Kind: proxyStopError,
132 | Error: fmt.Sprint(err),
133 | })
134 | }
135 |
136 | func (e *eventsHandler) OnUnsupportedDE(err error) {
137 | runtime.EventsEmit(e.ctx, proxyChannel, proxyAction{
138 | Kind: unsupportedDE,
139 | Error: fmt.Sprint(err),
140 | })
141 | }
142 |
143 | func (e *eventsHandler) OnUpdateAvailable() {
144 | runtime.EventsEmit(e.ctx, updateChannel, updateAction{
145 | Kind: updateAvailable,
146 | })
147 | }
148 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonGroup, FocusStyleManager, NonIdealState } from '@blueprintjs/core';
2 | import { useState, useEffect } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import './App.css';
6 |
7 | import { RestartApplication } from '../wailsjs/go/app/App';
8 | import { GetFirstLaunch } from '../wailsjs/go/cfg/Config';
9 | import { EventsOn } from '../wailsjs/runtime/runtime';
10 |
11 | import { ThemeType, useTheme } from './common/ThemeManager';
12 | import { AppToaster } from './common/toaster';
13 | import { AppHeader } from './components/AppHeader';
14 | import { useProxyState } from './context/ProxyStateContext';
15 | import { FilterLists } from './FilterLists';
16 | import { Intro } from './Intro';
17 | import { useProxyHotkey } from './ProxyHotkey';
18 | import { RequestLog } from './RequestLog';
19 | import { Rules } from './Rules';
20 | import { SettingsManager } from './SettingsManager';
21 | import { StartStopButton } from './StartStopButton';
22 |
23 | function App() {
24 | const { t } = useTranslation();
25 | const { effectiveTheme } = useTheme();
26 |
27 | useEffect(() => {
28 | FocusStyleManager.onlyShowFocusOnTabs();
29 | }, []);
30 |
31 | useEffect(() => {
32 | const cancel = EventsOn('app:update', (action: any) => {
33 | if (action.kind === 'updateAvailable') {
34 | AppToaster.show({
35 | message: t('app.update.updateAvailable'),
36 | intent: 'primary',
37 | timeout: 0,
38 | action: {
39 | text: t('app.update.restart'),
40 | onClick: () => {
41 | try {
42 | RestartApplication();
43 | } catch (error) {
44 | AppToaster.show({
45 | message: t('app.update.restartFailed', { error }),
46 | intent: 'danger',
47 | });
48 | }
49 | },
50 | },
51 | });
52 | }
53 | });
54 |
55 | return cancel;
56 | }, []);
57 |
58 | const { proxyState } = useProxyState();
59 | const [activeTab, setActiveTab] = useState<'home' | 'filterLists' | 'rules' | 'settings'>('home');
60 | const [showIntro, setShowIntro] = useState(false);
61 |
62 | useEffect(() => {
63 | GetFirstLaunch().then(setShowIntro);
64 | }, []);
65 |
66 | useProxyHotkey(showIntro);
67 |
68 | return (
69 |
70 |
71 |
72 | {showIntro ? (
73 |
{
75 | setShowIntro(false);
76 | }}
77 | />
78 | ) : (
79 | <>
80 |
81 |
84 |
87 |
90 |
93 |
94 |
95 |
96 |
97 | {proxyState === 'off' ? (
98 |
104 | ) : (
105 |
106 | )}
107 |
108 | {activeTab === 'filterLists' &&
}
109 | {activeTab === 'rules' &&
}
110 | {activeTab === 'settings' &&
}
111 |
112 |
113 | >
114 | )}
115 |
116 | );
117 | }
118 |
119 | export default App;
120 |
--------------------------------------------------------------------------------