├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── label-actions.yml └── workflows │ ├── issues-first-greet.yml │ ├── issues-label-actions.yml │ └── issues-stale.yml ├── .gitignore ├── .vscode └── extensions.json ├── AUTHORS ├── LICENSE ├── README.md ├── TRADEMARKS ├── android ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── capacitor.build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── beta │ │ └── res │ │ │ ├── ic_launcher-web.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-ldpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── playstore-icon.png │ │ │ └── values │ │ │ ├── ic_launcher_background.xml │ │ │ └── strings.xml │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── io │ │ │ └── safing │ │ │ └── portmaster │ │ │ └── android │ │ │ ├── Portmaster.java │ │ │ ├── connectivity │ │ │ ├── NetworkCallbacks.java │ │ │ └── PortmasterTunnelService.java │ │ │ ├── go_interface │ │ │ ├── Function.java │ │ │ ├── GoInterface.java │ │ │ └── Result.java │ │ │ ├── os │ │ │ ├── Address.java │ │ │ ├── NetInterface.java │ │ │ ├── NetworkAddresses.java │ │ │ ├── NetworkInterfaces.java │ │ │ ├── NetworkProxy.java │ │ │ ├── OSFunctions.java │ │ │ ├── PlatformInfo.java │ │ │ └── Shutdown.java │ │ │ ├── receiver │ │ │ └── SystemIdleEventReceiver.java │ │ │ ├── settings │ │ │ └── Settings.java │ │ │ ├── ui │ │ │ ├── GoBridge.java │ │ │ ├── GoPluginCall.java │ │ │ ├── JavaBridge.java │ │ │ └── MainActivity.java │ │ │ └── util │ │ │ ├── AppDir.java │ │ │ ├── CancelNotification.java │ │ │ ├── ConnectionOwner.java │ │ │ ├── DebugInfoDialog.java │ │ │ ├── GetAppUID.java │ │ │ ├── MinimizeApp.java │ │ │ ├── ServiceCommand.java │ │ │ ├── ShowNotification.java │ │ │ ├── UIEvent.java │ │ │ ├── VPNInit.java │ │ │ └── VPNProtect.java │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── notify_icon.png │ │ └── portmaster.xml │ │ ├── ic_launcher-web.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-ldpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── playstore-icon.png │ │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── file_paths.xml ├── build.gradle ├── capacitor.settings.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jni │ ├── arm64-v8a │ │ └── libgojni.so │ ├── armeabi-v7a │ │ └── libgojni.so │ ├── x86 │ │ └── libgojni.so │ └── x86_64 │ │ └── libgojni.so ├── settings.gradle └── variables.gradle ├── angular.json ├── capacitor.config.ts ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── go ├── app_interface │ ├── activity.go │ ├── connection.go │ ├── event.go │ ├── interface.go │ ├── network_address.go │ ├── network_interface.go │ ├── notification.go │ ├── os.go │ ├── platform_info.go │ └── service.go ├── build ├── codegen │ ├── gen.go │ ├── go.go │ ├── java.go │ └── ts.go ├── engine │ ├── bug_report │ │ └── report.go │ ├── events.go │ ├── init.go │ ├── logs │ │ └── module.go │ ├── module.go │ ├── tunnel │ │ ├── addr.go │ │ ├── default_route.go │ │ ├── module.go │ │ ├── resolver.go │ │ ├── router.go │ │ ├── spn_route.go │ │ └── state.go │ └── ui │ │ ├── exported │ │ └── proxy.go │ │ ├── functions.go │ │ └── structs.go ├── go.mod └── go.sum ├── ionic.config.json ├── ionic_android_build.sh ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.routes.ts │ ├── help │ │ ├── help.component.html │ │ ├── help.component.scss │ │ └── help.component.ts │ ├── lib │ │ ├── app-profile.service.ts │ │ ├── app-profile.types.ts │ │ ├── config.service.ts │ │ ├── config.types.ts │ │ ├── core.types.ts │ │ ├── debug-api.service.ts │ │ ├── meta-api.service.ts │ │ ├── module.ts │ │ ├── netquery.service.ts │ │ ├── network.types.ts │ │ ├── portapi.service.ts │ │ ├── portapi.types.ts │ │ ├── spn.service.ts │ │ ├── spn.types.ts │ │ ├── utils.ts │ │ └── websocket.service.ts │ ├── login │ │ ├── login.component.html │ │ ├── login.component.scss │ │ └── login.component.ts │ ├── menu │ │ ├── bug-report │ │ │ ├── bug-report.component.html │ │ │ ├── bug-report.component.scss │ │ │ └── bug-report.component.ts │ │ ├── enabled-apps │ │ │ ├── application.ts │ │ │ ├── enabled-apps.component.html │ │ │ ├── enabled-apps.component.scss │ │ │ ├── enabled-apps.component.spec.ts │ │ │ ├── enabled-apps.component.ts │ │ │ └── enabled-apps.filter.ts │ │ ├── logs │ │ │ ├── logs.component.html │ │ │ ├── logs.component.scss │ │ │ └── logs.component.ts │ │ ├── menu.component.html │ │ ├── menu.component.scss │ │ ├── menu.component.ts │ │ ├── user-info │ │ │ ├── user-info.component.html │ │ │ ├── user-info.component.scss │ │ │ └── user-info.component.ts │ │ └── vpn-settings │ │ │ ├── vpn-settings.component.html │ │ │ ├── vpn-settings.component.scss │ │ │ └── vpn-settings.component.ts │ ├── plugins │ │ ├── go.bridge.ts │ │ └── java.bridge.ts │ ├── services │ │ ├── http.backend.ts │ │ ├── notifications.service.ts │ │ ├── notifications.types.ts │ │ ├── shutdown.service.ts │ │ ├── status.service.ts │ │ ├── status.types.ts │ │ ├── updater.service.ts │ │ ├── updater.types.ts │ │ └── websocket.service.ts │ ├── settings │ │ ├── setting │ │ │ ├── config-settings.html │ │ │ ├── config-settings.scss │ │ │ ├── config-settings.ts │ │ │ ├── edit │ │ │ │ ├── edit.component.html │ │ │ │ ├── edit.component.scss │ │ │ │ └── edit.component.ts │ │ │ └── generic-setting │ │ │ │ ├── generic-setting.html │ │ │ │ ├── generic-setting.scss │ │ │ │ └── generic-setting.ts │ │ ├── settings.component.html │ │ ├── settings.component.scss │ │ └── settings.component.ts │ ├── spn-view │ │ ├── download-progress │ │ │ ├── download-progress.component.html │ │ │ ├── download-progress.component.scss │ │ │ └── download-progress.component.ts │ │ ├── notifications │ │ │ ├── notifications.component.html │ │ │ ├── notifications.component.scss │ │ │ └── notifications.component.ts │ │ ├── security-lock │ │ │ ├── security-lock.component.html │ │ │ ├── security-lock.component.scss │ │ │ └── security-lock.component.ts │ │ ├── spn-button │ │ │ ├── spn-button.component.html │ │ │ ├── spn-button.component.scss │ │ │ └── spn-button.component.ts │ │ ├── spn-view.component.html │ │ ├── spn-view.component.scss │ │ └── spn-view.component.ts │ ├── tabs │ │ ├── tabs.page.html │ │ ├── tabs.page.scss │ │ ├── tabs.page.ts │ │ └── tabs.routes.ts │ ├── types │ │ └── issue.types.ts │ └── welcome │ │ ├── welcome.component.html │ │ ├── welcome.component.scss │ │ └── welcome.component.ts ├── assets │ ├── access-regional-content-easily.png │ ├── always-on-setting.jpg │ ├── block-connections-setting.jpg │ ├── bye-bye-vpns.png │ ├── easily-control-your-privacy.png │ ├── icon │ │ └── favicon.png │ ├── main-vpn-settings.jpg │ ├── menu │ │ ├── activity.svg │ │ ├── bell.svg │ │ ├── help.svg │ │ ├── settings.svg │ │ ├── shield.svg │ │ ├── spn.svg │ │ └── user.svg │ ├── multiple-identities-for-each-app.png │ └── shapes.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss └── zone-flags.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | Chrome >=60 12 | Firefox >=63 13 | Edge >=79 14 | Safari >=13 15 | iOS >=13 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/ng-cli-compat", 13 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on", 14 | "plugin:@angular-eslint/template/process-inline-templates" 15 | ], 16 | "rules": { 17 | "@angular-eslint/component-class-suffix": [ 18 | "error", 19 | { 20 | "suffixes": ["Page", "Component"] 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "app", 28 | "style": "kebab-case" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "type": "attribute", 35 | "prefix": "app", 36 | "style": "camelCase" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": ["*.html"], 43 | "extends": ["plugin:@angular-eslint/template/recommended"], 44 | "rules": {} 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.github/label-actions.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Label Actions - https://github.com/dessant/label-actions 2 | 3 | community support: 4 | comment: | 5 | Hey @{issue-author}, thank you for raising this issue with us. 6 | 7 | After a first review we noticed that this does not seem to be a technical issue, but rather a configuration issue or general question about how Portmaster works. 8 | 9 | Thus, we invite the community to help with configuration and/or answering this questions. 10 | 11 | If you are in a hurry or haven't received an answer, a good place to ask is in [our Discord community](https://discord.gg/safing). 12 | 13 | If your problem or question has been resolved or answered, please come back and give an update here for other users encountering the same and then close this issue. 14 | 15 | If you are a paying subscriber and want this issue to be checked out by Safing, please send us a message [on Discord](https://discord.gg/safing) or [via Email](mailto:support@safing.io) with your username and the link to this issue, so we can prioritize accordingly. 16 | 17 | needs debug info: 18 | comment: | 19 | Hey @{issue-author}, thank you for raising this issue with us. 20 | 21 | After a first review we noticed that we will require the Debug Info for further investigation. However, you haven't supplied any Debug Info in your report. 22 | 23 | Please [collect Debug Info](https://wiki.safing.io/en/FAQ/DebugInfo) from Portmaster _while_ the reported issue is present. 24 | 25 | in/compatibility: 26 | comment: | 27 | Hey @{issue-author}, thank you for reporting on a compatibility. 28 | 29 | We keep a list of compatible software and user provided guides for improving compatibility [in the wiki - please have a look there](https://wiki.safing.io/en/Portmaster/App/Compatibility). 30 | If you can't find your software in the list, then a good starting point is our guide on [How do I make software compatible with Portmaster](https://wiki.safing.io/en/FAQ/MakeSoftwareCompatibleWithPortmaster). 31 | 32 | If you have managed to establish compatibility with an application, please share your findings here. This will greatly help other users encountering the same issues. 33 | 34 | fixed: 35 | comment: | 36 | This issue has been fixed by the recently referenced commit or PR. 37 | 38 | However, the fix is not released yet. 39 | 40 | It is expected to go into the [Beta Release Channel](https://wiki.safing.io/en/FAQ/SwitchReleaseChannel) for testing within the next two weeks and will be available for everyone within the next four weeks. While this is the typical timeline we work with, things are subject to change. 41 | -------------------------------------------------------------------------------- /.github/workflows/issues-first-greet.yml: -------------------------------------------------------------------------------- 1 | # This workflow responds to first time posters with a greeting message. 2 | # Docs: https://github.com/actions/first-interaction 3 | name: Greet New Users 4 | 5 | # This workflow is triggered when a new issue is created. 6 | on: 7 | issues: 8 | types: opened 9 | 10 | permissions: 11 | contents: read 12 | issues: write 13 | 14 | jobs: 15 | greet: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/first-interaction@v1 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | # Respond to first time issue raisers. 22 | issue-message: | 23 | Greetings and welcome to our community! As this is the first issue you opened here, we wanted to share some useful infos with you: 24 | 25 | - 🗣️ Our community on [Discord](https://discord.gg/safing) is super helpful and active. We also have an AI-enabled support bot that knows Portmaster well and can give you immediate help. 26 | - 📖 The [Wiki](https://wiki.safing.io/) answers all common questions and has many important details. If you can't find an answer there, let us know, so we can add anything that's missing. 27 | -------------------------------------------------------------------------------- /.github/workflows/issues-label-actions.yml: -------------------------------------------------------------------------------- 1 | # This workflow responds with a message when certain labels are added to an issue or PR. 2 | # Docs: https://github.com/dessant/label-actions 3 | name: Label Actions 4 | 5 | # This workflow is triggered when a label is added to an issue. 6 | on: 7 | issues: 8 | types: labeled 9 | 10 | permissions: 11 | contents: read 12 | issues: write 13 | 14 | jobs: 15 | action: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: dessant/label-actions@v3 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | config-path: ".github/label-actions.yml" 22 | process-only: "issues" 23 | -------------------------------------------------------------------------------- /.github/workflows/issues-stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes stale issues and PRs. 2 | # Docs: https://github.com/actions/stale 3 | name: Close Stale Issues 4 | 5 | on: 6 | schedule: 7 | - cron: "17 5 * * 1-5" # run at 5:17 (UTC) on Monday to Friday 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | issues: write 13 | 14 | jobs: 15 | stale: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/stale@v8 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | # Increase max operations. 22 | # When using GITHUB_TOKEN, the rate limit is 1,000 requests per hour per repository. 23 | operations-per-run: 500 24 | # Handle stale issues 25 | stale-issue-label: 'stale' 26 | # Exemptions 27 | exempt-all-issue-assignees: true 28 | exempt-issue-labels: 'support,dependencies,pinned,security' 29 | # Mark as stale 30 | days-before-issue-stale: 63 # 2 months / 9 weeks 31 | stale-issue-message: | 32 | This issue has been automatically marked as inactive because it has not had activity in the past two months. 33 | 34 | If no further activity occurs, this issue will be automatically closed in one week in order to increase our focus on active topics. 35 | # Close 36 | days-before-issue-close: 7 # 1 week 37 | close-issue-message: | 38 | This issue has been automatically closed because it has not had recent activity. Thank you for your contributions. 39 | 40 | If the issue has not been resolved, you can [find more information in our Wiki](https://wiki.safing.io/) or [continue the conversation on our Discord](https://discord.gg/safing). 41 | # TODO: Handle stale PRs 42 | days-before-pr-stale: 36500 # 100 years - effectively disabled. 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | .tmp 7 | *.tmp 8 | *.tmp.* 9 | *.sublime-project 10 | *.sublime-workspace 11 | .DS_Store 12 | Thumbs.db 13 | UserInterfaceState.xcuserstate 14 | $RECYCLE.BIN/ 15 | 16 | *.log 17 | log.txt 18 | npm-debug.log* 19 | 20 | /.angular 21 | /.idea 22 | /.ionic 23 | /.sass-cache 24 | /.sourcemaps 25 | /.versions 26 | /.vscode/* 27 | !/.vscode/extensions.json 28 | /coverage 29 | /dist 30 | /node_modules 31 | /platforms 32 | /plugins 33 | /www 34 | android/.idea 35 | android/portmaster/release 36 | android/portmaster/src/main/assets/public 37 | android/app/libs/golib-sources.jar 38 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ionic.ionic" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | All files in this repository (unless otherwise noted) are authored, owned and copyrighted by Safing ICS Technologies GmbH (Austria). 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Check out the main project repository [safing/portmaster](https://github.com/safing/portmaster)** 2 | 3 | # Portmaster Android 4 | 5 | Portmaster Android is free and open-source app that lets you connect to the [SPN](https://docs.safing.io/portmaster/architecture/core-service/spn). 6 | -------------------------------------------------------------------------------- /TRADEMARKS: -------------------------------------------------------------------------------- 1 | The names "Safing", "Portmaster", "Gate17" and their logos are trademarks owned by Safing ICS Technologies GmbH (Austria). 2 | 3 | Although our code is free, it is very important that we strictly enforce our trademark rights, in order to be able to protect our users against people who use the marks to commit fraud. This means that, while you have considerable freedom to redistribute and modify our software, there are tight restrictions on your ability to use our names and logos in ways which fall in the domain of trademark law, even when built into binaries that we provide. 4 | 5 | This file is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. Parts of it were taken from https://www.mozilla.org/en-US/foundation/licensing/. 6 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore 2 | 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | # Android Profiling 90 | *.hprof 91 | 92 | # Cordova plugins for Capacitor 93 | capacitor-cordova-android-plugins 94 | 95 | # Copied web assets 96 | app/src/main/assets/public 97 | 98 | # Generated Config files 99 | app/src/main/assets/capacitor.config.json 100 | app/src/main/assets/capacitor.plugins.json 101 | app/src/main/res/xml/config.xml 102 | -------------------------------------------------------------------------------- /android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.npmkeep 3 | -------------------------------------------------------------------------------- /android/app/capacitor.build.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility JavaVersion.VERSION_11 6 | targetCompatibility JavaVersion.VERSION_11 7 | } 8 | } 9 | 10 | apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" 11 | dependencies { 12 | implementation project(':capacitor-app') 13 | implementation project(':capacitor-haptics') 14 | implementation project(':capacitor-keyboard') 15 | implementation project(':capacitor-status-bar') 16 | 17 | } 18 | 19 | 20 | if (hasProperty('postBuildExtras')) { 21 | postBuildExtras() 22 | } 23 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/app/src/beta/res/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/ic_launcher-web.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/beta/res/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/beta/res/playstore-icon.png -------------------------------------------------------------------------------- /android/app/src/beta/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /android/app/src/beta/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Portmaster Beta 4 | Portmaster Beta 5 | io.safing.portmaster.android.beta 6 | io.safing.portmaster.android.beta 7 | Server Address: 8 | Server Port: 9 | Shared Secret: 10 | Connect! 11 | Disconnect! 12 | HTTP proxy hostname 13 | HTTP proxy port 14 | Packages (comma separated): 15 | Allow 16 | Disallow 17 | Portmaster is connecting... 18 | Portmaster is connected! 19 | Portmaster is disconnected! 20 | 21 | Incomplete proxy settings. For HTTP proxy we require both hostname and port settings. 22 | 23 | 24 | Some of the specified package names do not correspond to any installed packages. 25 | 26 | portmaster.beta 27 | Portmaster Beta firewall 28 | 29 | 30 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/Portmaster.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | public class Portmaster extends Application { 7 | 8 | @Override 9 | protected void attachBaseContext(Context base) { 10 | super.attachBaseContext(base); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/connectivity/NetworkCallbacks.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.connectivity; 2 | 3 | import android.net.ConnectivityManager; 4 | import android.net.Network; 5 | import android.net.NetworkCapabilities; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import engine.Engine; 10 | import io.safing.portmaster.android.os.NetworkProxy; 11 | 12 | public class NetworkCallbacks extends ConnectivityManager.NetworkCallback { 13 | @Override 14 | public void onAvailable(@NonNull Network network) { 15 | super.onAvailable(network); 16 | Engine.onNetworkConnected(); 17 | } 18 | 19 | @Override 20 | public void onLost(@NonNull Network network) { 21 | super.onLost(network); 22 | Engine.onNetworkDisconnected(); 23 | } 24 | 25 | @Override 26 | public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { 27 | super.onCapabilitiesChanged(network, networkCapabilities); 28 | Engine.onNetworkCapabilitiesChanged(new NetworkProxy(networkCapabilities)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/go_interface/Function.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.go_interface; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.exc.StreamReadException; 5 | import com.fasterxml.jackson.databind.DatabindException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; 8 | 9 | import java.io.IOException; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | public abstract class Function { 13 | private String name; 14 | private ObjectMapper mapper = new CBORMapper(); 15 | 16 | protected Function(String name) { 17 | this.name = name; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | protected byte[] toResultFromObject(Object obj) throws RuntimeException { 25 | byte[] data = null; 26 | try { 27 | data = this.mapper.writeValueAsBytes(obj); 28 | } catch (JsonProcessingException e) { 29 | throw new RuntimeException("failed to convert to CDOR: " + e.getMessage()); 30 | } 31 | 32 | return data; 33 | } 34 | 35 | public T parseArguments(byte[] args, Class valueType) throws Exception { 36 | return mapper.readValue(args, valueType); 37 | } 38 | 39 | public abstract byte[] call(byte[] args) throws Exception; 40 | } 41 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/go_interface/GoInterface.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.go_interface; 2 | 3 | import android.content.Context; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; 8 | 9 | import java.util.HashMap; 10 | 11 | 12 | public class GoInterface implements app_interface.AppInterface { 13 | 14 | private Context context; 15 | private ObjectMapper mapper = new CBORMapper(); 16 | 17 | private HashMap functions = new HashMap<>(); 18 | 19 | 20 | public void registerFunction(Function func) { 21 | this.functions.put(func.getName(), func); 22 | } 23 | 24 | @Override 25 | public byte[] callFunction(String functionName, byte[] bytes) throws Exception { 26 | // Get requested function 27 | Function func = functions.get(functionName); 28 | // Call the requested function if found and extract the result 29 | if(func == null) { 30 | throw new RuntimeException("function " + functionName + " not implemented"); 31 | } 32 | 33 | return func.call(bytes); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/go_interface/Result.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.go_interface; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; 6 | 7 | import java.nio.charset.StandardCharsets; 8 | 9 | public class Result { 10 | public byte[] data; 11 | public String error; 12 | 13 | public Result(byte[] data, String error) { 14 | this.data = data; 15 | this.error = error; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/Address.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | public class Address { 4 | public Address(String address, int prefixLength, boolean isIPv6) { 5 | this.addr = address; 6 | this.prefixLength = prefixLength; 7 | this.isIPv6 = isIPv6; 8 | } 9 | public String addr; 10 | public int prefixLength; 11 | public boolean isIPv6; 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/NetInterface.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class NetInterface { 7 | public String name; 8 | public int index; 9 | public int MTU; 10 | public boolean up; 11 | public boolean multicast; 12 | public boolean loopback; 13 | public boolean p2p; 14 | public List
addresses = new ArrayList<>(); 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/NetworkAddresses.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import java.io.IOException; 4 | import java.net.InterfaceAddress; 5 | import java.net.NetworkInterface; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import io.safing.portmaster.android.go_interface.Function; 11 | import io.safing.portmaster.android.go_interface.Result; 12 | 13 | public class NetworkAddresses extends Function { 14 | 15 | public NetworkAddresses(String name) { 16 | super(name); 17 | } 18 | 19 | public static List
getInterfaceAddresses(NetworkInterface nif) { 20 | List
addresses = new ArrayList<>(); 21 | for (InterfaceAddress ia : nif.getInterfaceAddresses()) { 22 | String addr = ia.getAddress().getHostAddress(); 23 | int index = addr.lastIndexOf("%"); 24 | if (index > 0) { 25 | addr = addr.substring(0, index); 26 | } 27 | 28 | boolean isIPv6 = addr.contains(":"); 29 | addresses.add(new Address(addr, ia.getNetworkPrefixLength(), isIPv6)); 30 | } 31 | return addresses; 32 | } 33 | 34 | @Override 35 | public byte[] call(byte[] args) throws Exception { 36 | List interfaces = null; 37 | try { 38 | interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); 39 | } catch (Exception e) { 40 | return null; 41 | } 42 | 43 | List
addresses = new ArrayList<>(); 44 | for (NetworkInterface nif : interfaces) { 45 | addresses.addAll(getInterfaceAddresses(nif)); 46 | } 47 | 48 | return toResultFromObject(addresses); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/NetworkInterfaces.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import java.net.NetworkInterface; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import io.safing.portmaster.android.go_interface.Function; 9 | 10 | public class NetworkInterfaces extends Function { 11 | 12 | public NetworkInterfaces(String name) { 13 | super(name); 14 | } 15 | 16 | @Override 17 | public byte[] call(byte[] args) throws Exception { 18 | List interfaces = null; 19 | try { 20 | interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); 21 | } catch (Exception e) { 22 | return null; 23 | } 24 | 25 | List netInterfaces = new ArrayList<>(); 26 | for (NetworkInterface nif : interfaces) { 27 | try { 28 | NetInterface inf = new NetInterface(); 29 | inf.name = nif.getName(); 30 | inf.index = nif.getIndex(); 31 | inf.MTU = nif.getMTU(); 32 | inf.multicast = nif.supportsMulticast(); 33 | inf.loopback = true; 34 | inf.p2p = nif.isPointToPoint(); 35 | inf.addresses = NetworkAddresses.getInterfaceAddresses(nif); 36 | netInterfaces.add(inf); 37 | } catch (Exception e) { 38 | continue; 39 | } 40 | } 41 | 42 | return toResultFromObject(netInterfaces); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/NetworkProxy.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import android.net.NetworkCapabilities; 4 | 5 | public class NetworkProxy implements engine.Network { 6 | private NetworkCapabilities network = null; 7 | 8 | public NetworkProxy(NetworkCapabilities network) { 9 | this.network = network; 10 | } 11 | 12 | @Override 13 | public boolean hasCapability(int capability) { 14 | return network.hasCapability(capability); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/OSFunctions.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import io.safing.portmaster.android.go_interface.GoInterface; 4 | 5 | public class OSFunctions { 6 | 7 | public static GoInterface get() { 8 | GoInterface osInterface = new GoInterface(); 9 | osInterface.registerFunction(new NetworkInterfaces("GetNetworkInterfaces")); 10 | osInterface.registerFunction(new NetworkAddresses("GetNetworkAddresses")); 11 | osInterface.registerFunction(new PlatformInfo("GetPlatformInfo")); 12 | osInterface.registerFunction(new Shutdown("Shutdown")); 13 | return osInterface; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/PlatformInfo.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import android.os.Build; 4 | 5 | import io.safing.portmaster.android.BuildConfig; 6 | import io.safing.portmaster.android.go_interface.Function; 7 | import io.safing.portmaster.android.go_interface.Result; 8 | 9 | public class PlatformInfo extends Function { 10 | 11 | class Info { 12 | public String Model; 13 | public String Manufacturer; 14 | public String Brand; 15 | public String Board; 16 | public int SDK; 17 | public int VersionCode; 18 | public String VersionName; 19 | public String ApplicationID; 20 | public String BuildType; 21 | } 22 | 23 | public PlatformInfo(String name) { 24 | super(name); 25 | } 26 | 27 | @Override 28 | public byte[] call(byte[] args) throws Exception { 29 | Info info = new Info(); 30 | info.Model = Build.MODEL; 31 | info.Manufacturer = Build.MANUFACTURER; 32 | info.Brand = Build.BRAND; 33 | info.Board = Build.BOARD; 34 | info.SDK = Build.VERSION.SDK_INT; 35 | info.VersionCode = BuildConfig.VERSION_CODE; 36 | info.VersionName = BuildConfig.VERSION_NAME; 37 | info.ApplicationID = BuildConfig.APPLICATION_ID; 38 | info.BuildType = BuildConfig.BUILD_TYPE; 39 | 40 | return toResultFromObject(info); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/os/Shutdown.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.os; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | 6 | import io.safing.portmaster.android.connectivity.PortmasterTunnelService; 7 | import io.safing.portmaster.android.go_interface.Function; 8 | import io.safing.portmaster.android.ui.MainActivity; 9 | 10 | public class Shutdown extends Function { 11 | 12 | public Shutdown(String name) { 13 | super(name); 14 | } 15 | 16 | @Override 17 | public byte[] call(byte[] args) throws Exception { 18 | System.exit(0); 19 | return null; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/receiver/SystemIdleEventReceiver.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.receiver; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Build; 7 | import android.os.PowerManager; 8 | import android.util.Log; 9 | 10 | import engine.Engine; 11 | 12 | public class SystemIdleEventReceiver extends BroadcastReceiver { 13 | 14 | @Override 15 | public void onReceive(Context context, Intent intent) { 16 | PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 18 | Engine.onIdleModeChanged(pm.isDeviceIdleMode()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/settings/Settings.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.settings; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | import java.util.HashSet; 10 | import java.util.Set; 11 | 12 | import io.safing.portmaster.android.ui.MainActivity; 13 | 14 | public class Settings { 15 | private static final String DISABLED_APPS_SETTINGS_KEY = "DisabledAppsSettingsKey"; 16 | private static final String WELCOME_SCREEN_SHOWED = "WelcomeScreenShowedKey"; 17 | 18 | public static Set getDisabledApps(Context context) { 19 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 20 | Set disabledApps = settings.getStringSet(DISABLED_APPS_SETTINGS_KEY, new HashSet<>()); 21 | return disabledApps; 22 | } 23 | 24 | public static void setDisabledApps(Context context, Set disabledApps) { 25 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 26 | SharedPreferences.Editor editor = settings.edit(); 27 | editor.putStringSet(DISABLED_APPS_SETTINGS_KEY, disabledApps); 28 | editor.commit(); 29 | } 30 | 31 | public static boolean ShouldShowWelcomeScreen(Context context) { 32 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 33 | return !settings.getBoolean(WELCOME_SCREEN_SHOWED, false); 34 | } 35 | 36 | public static void setWelcomeScreenShowed(Context context, boolean showed) { 37 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 38 | SharedPreferences.Editor editor = settings.edit(); 39 | editor.putBoolean(WELCOME_SCREEN_SHOWED, showed); 40 | editor.commit(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/ui/GoPluginCall.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.ui; 2 | 3 | import android.util.Log; 4 | 5 | import com.getcapacitor.JSObject; 6 | import com.getcapacitor.PluginCall; 7 | 8 | import org.json.JSONException; 9 | 10 | public class GoPluginCall implements engine.PluginCall { 11 | 12 | private PluginCall call; 13 | private GoBridge plugin; 14 | 15 | public GoPluginCall(GoBridge plugin, PluginCall call) { 16 | this.call = call; 17 | this.plugin = plugin; 18 | } 19 | 20 | @Override 21 | public String getArgs() { 22 | return call.getData().toString(); 23 | } 24 | 25 | @Override 26 | public boolean getBool(String s) { 27 | checkArgument(s); 28 | return call.getBoolean(s); 29 | } 30 | 31 | @Override 32 | public float getFloat(String s) { 33 | checkArgument(s); 34 | return call.getFloat(s); 35 | } 36 | 37 | @Override 38 | public int getInt(String s) { 39 | checkArgument(s); 40 | return call.getInt(s); 41 | } 42 | 43 | @Override 44 | public long getLong(String s) { 45 | checkArgument(s); 46 | Long result = call.getLong(s); 47 | if(result == null) { 48 | result = call.getInt(s).longValue(); 49 | } 50 | return result; 51 | } 52 | 53 | @Override 54 | public String getString(String s) { 55 | checkArgument(s); 56 | return call.getString(s); 57 | } 58 | 59 | @Override 60 | public void resolve() { 61 | call.resolve(); 62 | } 63 | 64 | @Override 65 | public void resolveJson(String obj) { 66 | try { 67 | call.resolve(new JSObject(obj)); 68 | } catch (JSONException ex) { 69 | ex.printStackTrace(); 70 | } 71 | } 72 | 73 | @Override 74 | public void error(String err) { 75 | call.reject(err); 76 | } 77 | 78 | @Override 79 | public void keepAlive(boolean keepAlive) { 80 | this.call.setKeepAlive(keepAlive); 81 | } 82 | 83 | @Override 84 | public void notify(String eventName, String data) throws JSONException { 85 | this.plugin.notifyListener(eventName, data); 86 | } 87 | 88 | private void checkArgument(String arg) { 89 | if(!this.call.hasOption(arg)) { 90 | throw new IllegalArgumentException("No argument with name: " + arg); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/AppDir.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | import io.safing.portmaster.android.go_interface.Function; 9 | import io.safing.portmaster.android.go_interface.Result; 10 | 11 | public class AppDir extends Function { 12 | 13 | private Context context; 14 | 15 | public AppDir(String name, Context context) { 16 | super(name); 17 | this.context = context; 18 | } 19 | 20 | @Override 21 | public byte[] call(byte[] args) throws Exception { 22 | String appDir = this.context.getFilesDir().getAbsolutePath(); 23 | return appDir.getBytes(StandardCharsets.UTF_8); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/CancelNotification.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.core.app.NotificationManagerCompat; 6 | 7 | import io.safing.portmaster.android.go_interface.Function; 8 | 9 | public class CancelNotification extends Function { 10 | private Context context; 11 | 12 | public CancelNotification(String name, Context context) { 13 | super(name); 14 | this.context = context; 15 | } 16 | 17 | @Override 18 | public byte[] call(byte[] args) throws Exception { 19 | int deleteId = parseArguments(args, int.class); 20 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this.context); 21 | notificationManager.cancel(deleteId); 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/ConnectionOwner.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.net.ConnectivityManager; 4 | import android.os.Build; 5 | import android.os.Process; 6 | import android.system.OsConstants; 7 | 8 | import java.net.InetAddress; 9 | import java.net.InetSocketAddress; 10 | 11 | import io.safing.portmaster.android.go_interface.Function; 12 | 13 | public class ConnectionOwner extends Function { 14 | 15 | public static class Connection { 16 | public int Protocol; 17 | public byte[] LocalIP; 18 | public int LocalPort; 19 | public byte[] RemoteIP; 20 | public int RemotePort; 21 | } 22 | 23 | ConnectivityManager connectivityManager; 24 | 25 | public ConnectionOwner(String name, ConnectivityManager connectivityManager) { 26 | super(name); 27 | this.connectivityManager = connectivityManager; 28 | } 29 | 30 | @Override 31 | public byte[] call(byte[] args) throws Exception { 32 | Connection connection = parseArguments(args, Connection.class); 33 | 34 | if(connection.LocalIP == null || connection.LocalIP.length == 0) { 35 | throw new IllegalArgumentException("invalid local IP"); 36 | } 37 | 38 | if(connection.RemoteIP == null || connection.RemoteIP.length == 0) { 39 | throw new IllegalArgumentException("invalid remote IP"); 40 | } 41 | 42 | InetSocketAddress local = new InetSocketAddress(InetAddress.getByAddress(connection.LocalIP), connection.LocalPort); 43 | InetSocketAddress remote = new InetSocketAddress(InetAddress.getByAddress(connection.RemoteIP), connection.RemotePort); 44 | 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 46 | int uid = connectivityManager.getConnectionOwnerUid(connection.Protocol, local, remote); 47 | 48 | if(uid == Process.INVALID_UID) { 49 | throw new RuntimeException("invalid connection info"); 50 | } 51 | 52 | return this.toResultFromObject(uid); 53 | 54 | } else { 55 | throw new RuntimeException("not implemented for android sdk < 29"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/DebugInfoDialog.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | 7 | import java.io.FileInputStream; 8 | import java.io.FileNotFoundException; 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | 12 | import io.safing.portmaster.android.go_interface.Function; 13 | import io.safing.portmaster.android.go_interface.Result; 14 | 15 | public class DebugInfoDialog extends Function { 16 | 17 | static class Args { 18 | public String Filename; 19 | public byte[] Content; 20 | } 21 | 22 | private int activityID; 23 | private Activity activity; 24 | private byte[] content; 25 | 26 | public DebugInfoDialog(String name, Activity activity, int activityID) { 27 | super(name); 28 | this.activity = activity; 29 | this.activityID = activityID; 30 | } 31 | 32 | @Override 33 | public byte[] call(byte[] args) throws Exception { 34 | Args parsedArgs = this.parseArguments(args, Args.class); 35 | String filename = parsedArgs.Filename; 36 | this.content = parsedArgs.Content; 37 | 38 | Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 39 | intent.addCategory(Intent.CATEGORY_OPENABLE); 40 | intent.setType("text/plain"); 41 | intent.putExtra(Intent.EXTRA_TITLE, filename); 42 | activity.startActivityForResult(intent, this.activityID); 43 | 44 | return null; 45 | } 46 | 47 | public void writeToFile(Uri uri) { 48 | OutputStream output = null; 49 | try { 50 | output = activity.getContentResolver().openOutputStream(uri); 51 | output.write(this.content); 52 | output.close(); 53 | } catch (IOException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/GetAppUID.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.PackageManager; 6 | 7 | import io.safing.portmaster.android.go_interface.Function; 8 | 9 | public class GetAppUID extends Function { 10 | 11 | private Context context = null; 12 | public GetAppUID(String name, Context context) { 13 | super(name); 14 | this.context = context; 15 | } 16 | 17 | @Override 18 | public byte[] call(byte[] args) throws Exception { 19 | ApplicationInfo info = this.context.getPackageManager().getApplicationInfo( 20 | context.getPackageName(), 0); 21 | 22 | return this.toResultFromObject(info.uid); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/MinimizeApp.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.app.Activity; 4 | import io.safing.portmaster.android.go_interface.Function; 5 | 6 | public class MinimizeApp extends Function { 7 | 8 | private Activity activity; 9 | 10 | public MinimizeApp(String name, Activity activity) { 11 | super(name); 12 | this.activity = activity; 13 | } 14 | 15 | @Override 16 | public byte[] call(byte[] args) throws Exception { 17 | this.activity.moveTaskToBack(false); 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/ServiceCommand.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.content.Intent; 4 | import android.net.VpnService; 5 | 6 | 7 | import io.safing.portmaster.android.connectivity.PortmasterTunnelService; 8 | import io.safing.portmaster.android.go_interface.Function; 9 | import io.safing.portmaster.android.ui.MainActivity; 10 | 11 | public class ServiceCommand extends Function { 12 | 13 | private MainActivity activity; 14 | 15 | public ServiceCommand(String name, MainActivity activity) { 16 | super(name); 17 | this.activity = activity; 18 | } 19 | 20 | @Override 21 | public byte[] call(byte[] args) throws Exception { 22 | String command = parseArguments(args, String.class); 23 | this.send(command); 24 | return null; 25 | } 26 | 27 | public void send(String command) { 28 | // Check if VPN Service has permissions 29 | Intent intent = VpnService.prepare(activity.getApplicationContext()); 30 | if(intent != null) { 31 | // Put the requested command 32 | intent.putExtra("command", command); 33 | // Request user permissions 34 | activity.startActivityForResult(intent, MainActivity.ENABLE_VPN); 35 | return; 36 | } 37 | 38 | // User already approved the permissions, send the command. 39 | intent = new Intent(activity, PortmasterTunnelService.class); 40 | intent.setAction(PortmasterTunnelService.COMMAND_PREFIX + command); 41 | activity.startService(intent); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/ShowNotification.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | 8 | import androidx.core.app.NotificationCompat; 9 | import androidx.core.app.NotificationManagerCompat; 10 | 11 | import io.safing.portmaster.android.R; 12 | import io.safing.portmaster.android.go_interface.Function; 13 | import io.safing.portmaster.android.ui.MainActivity; 14 | 15 | public class ShowNotification extends Function { 16 | 17 | public static class Args { 18 | public int ID; 19 | public String Title; 20 | public String Message; 21 | } 22 | 23 | private Context context; 24 | 25 | public ShowNotification(String name, Context context) { 26 | super(name); 27 | this.context = context; 28 | } 29 | 30 | @Override 31 | public byte[] call(byte[] args) throws Exception { 32 | Args notificationArgs = parseArguments(args, ShowNotification.Args.class); 33 | 34 | Intent intent = new Intent(this.context, MainActivity.class); 35 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 36 | PendingIntent pendingIntent = PendingIntent.getActivity(this.context, 0, intent, PendingIntent.FLAG_IMMUTABLE); 37 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this.context, MainActivity.CHANNEL_ID) 38 | .setSmallIcon(R.drawable.notify_icon) 39 | .setContentTitle(notificationArgs.Title) 40 | .setContentText(notificationArgs.Message) 41 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 42 | .setContentIntent(pendingIntent) 43 | .setStyle(new NotificationCompat.BigTextStyle() 44 | .bigText(notificationArgs.Message)) 45 | .setAutoCancel(true); 46 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this.context); 47 | notificationManager.notify(notificationArgs.ID, builder.build()); 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/UIEvent.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import com.getcapacitor.Bridge; 4 | 5 | 6 | import io.safing.portmaster.android.go_interface.Function; 7 | 8 | public class UIEvent extends Function { 9 | 10 | private Bridge bridge; 11 | 12 | public static class Event { 13 | public Event() {} 14 | public String Name; 15 | public String Target; 16 | public String Data; 17 | } 18 | 19 | public UIEvent(String name, Bridge bridge) { 20 | super(name); 21 | this.bridge = bridge; 22 | } 23 | 24 | @Override 25 | public byte[] call(byte[] data) throws Exception { 26 | Event args = this.parseArguments(data, Event.class); 27 | this.bridge.triggerJSEvent(args.Name, args.Target, args.Data); 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/VPNInit.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import io.safing.portmaster.android.connectivity.PortmasterTunnelService; 4 | import io.safing.portmaster.android.go_interface.Function; 5 | 6 | public class VPNInit extends Function { 7 | 8 | private PortmasterTunnelService service; 9 | 10 | public VPNInit(String name, PortmasterTunnelService service) { 11 | super(name); 12 | this.service = service; 13 | } 14 | 15 | @Override 16 | public byte[] call(byte[] args) throws Exception { 17 | int fd = service.InitVPN(); 18 | return toResultFromObject(fd); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/safing/portmaster/android/util/VPNProtect.java: -------------------------------------------------------------------------------- 1 | package io.safing.portmaster.android.util; 2 | 3 | import android.net.VpnService; 4 | 5 | import io.safing.portmaster.android.go_interface.Function; 6 | 7 | public class VPNProtect extends Function { 8 | 9 | private VpnService service; 10 | 11 | public VPNProtect(String name, VpnService service) { 12 | super(name); 13 | this.service = service; 14 | } 15 | 16 | @Override 17 | public byte[] call(byte[] args) throws Exception { 18 | int socketID = parseArguments(args, int.class); 19 | boolean success = service.protect(socketID); 20 | 21 | if(!success) { 22 | throw new RuntimeException("failed to protect socket"); 23 | } 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/notify_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/drawable/notify_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/portmaster.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /android/app/src/main/res/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/ic_launcher-web.png -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/app/src/main/res/playstore-icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Portmaster 4 | Portmaster 5 | io.safing.portmaster.android 6 | io.safing.portmaster.android 7 | Server Address: 8 | Server Port: 9 | Shared Secret: 10 | Connect! 11 | Disconnect! 12 | HTTP proxy hostname 13 | HTTP proxy port 14 | Packages (comma separated): 15 | Allow 16 | Disallow 17 | Portmaster is connecting... 18 | Portmaster is connected! 19 | Portmaster is disconnected! 20 | 21 | Incomplete proxy settings. For HTTP proxy we require both hostname and port settings. 22 | 23 | 24 | Some of the specified package names do not correspond to any installed packages. 25 | 26 | portmaster 27 | Portmaster firewall 28 | 29 | 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 17 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:7.2.1' 11 | classpath 'com.google.gms:google-services:4.3.13' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | apply from: "variables.gradle" 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | 31 | -------------------------------------------------------------------------------- /android/capacitor.settings.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | include ':capacitor-android' 3 | project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 4 | 5 | include ':capacitor-app' 6 | project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') 7 | 8 | include ':capacitor-haptics' 9 | project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') 10 | 11 | include ':capacitor-keyboard' 12 | project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') 13 | 14 | include ':capacitor-status-bar' 15 | project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') 16 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | # Automatically convert third-party libraries to use AndroidX 24 | android.enableJetifier=true 25 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/jni/arm64-v8a/libgojni.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/jni/arm64-v8a/libgojni.so -------------------------------------------------------------------------------- /android/jni/armeabi-v7a/libgojni.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/jni/armeabi-v7a/libgojni.so -------------------------------------------------------------------------------- /android/jni/x86/libgojni.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/jni/x86/libgojni.so -------------------------------------------------------------------------------- /android/jni/x86_64/libgojni.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/android/jni/x86_64/libgojni.so -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':capacitor-cordova-android-plugins' 3 | project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') 4 | 5 | apply from: 'capacitor.settings.gradle' 6 | -------------------------------------------------------------------------------- /android/variables.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | minSdkVersion = 22 3 | compileSdkVersion = 32 4 | targetSdkVersion = 32 5 | androidxActivityVersion = '1.4.0' 6 | androidxAppCompatVersion = '1.4.2' 7 | androidxCoordinatorLayoutVersion = '1.2.0' 8 | androidxCoreVersion = '1.8.0' 9 | androidxFragmentVersion = '1.4.1' 10 | coreSplashScreenVersion = '1.0.0-rc01' 11 | androidxWebkitVersion = '1.4.0' 12 | junitVersion = '4.13.2' 13 | androidxJunitVersion = '1.1.3' 14 | androidxEspressoCoreVersion = '3.4.0' 15 | cordovaAndroidVersion = '10.1.1' 16 | } -------------------------------------------------------------------------------- /capacitor.config.ts: -------------------------------------------------------------------------------- 1 | import { CapacitorConfig } from '@capacitor/cli'; 2 | 3 | const config: CapacitorConfig = { 4 | appId: 'io.safing.portmaster.android', 5 | appName: 'Portmaster', 6 | webDir: 'www', 7 | bundledWebRuntime: false, 8 | loggingBehavior: 'none', 9 | // server: { 10 | // url: "http://192.168.88.11:8100", 11 | // cleartext: true 12 | // }, 13 | // cordova: { 14 | // preferences: { 15 | // "KeepAlive": "false" // Determines whether the application stays running in the background even after a pause event fires. Setting this to false does not kill the app after a pause event, but simply halts execution of code within the cordova webview while the app is in the background. (https://cordova.apache.org/docs/en/latest/config_ref/) 16 | // } 17 | // } 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getPageTitle()).toContain('Tab 1'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getPageTitle() { 9 | return element(by.css('ion-title')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /go/app_interface/activity.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fxamacker/cbor/v2" 7 | ) 8 | 9 | var activityFunctions *AppFunctions = nil 10 | 11 | func SetActivityFunctions(appInterface AppInterface) { 12 | activityFunctions = &AppFunctions{javaInterface: appInterface} 13 | } 14 | 15 | func HasActivityFunctions() bool { 16 | return activityFunctions != nil 17 | } 18 | 19 | func RemoveActivityFunctionReference() { 20 | activityFunctions = nil 21 | } 22 | 23 | func ExportDebugInfo(filename string, content []byte) error { 24 | var args = struct { 25 | Filename string 26 | Content []byte 27 | }{Filename: filename, Content: content} 28 | 29 | argsBytes, err := cbor.Marshal(args) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | _, err = activityFunctions.call("ExportDebugInfo", argsBytes) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func SendServicesCommand(command string) error { 42 | args, _ := cbor.Marshal(command) 43 | 44 | _, err := activityFunctions.call("SendServiceCommand", args) 45 | if err != nil { 46 | return fmt.Errorf("failed to send service command: %s", err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func SendUIEvent(event Event) error { 53 | args, _ := cbor.Marshal(event) 54 | 55 | _, err := activityFunctions.call("SendUIEvent", args) 56 | if err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | func SendUIWindowEvent(name, data string) error { 63 | return SendUIEvent(Event{Name: name, Target: "window", Data: data}) 64 | } 65 | 66 | func MinimizeApp() error { 67 | _, err := activityFunctions.call("MinimizeApp", nil) 68 | if err != nil { 69 | return fmt.Errorf("failed to minimize app: %s", err) 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /go/app_interface/connection.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import "net" 4 | 5 | type Connection struct { 6 | Protocol int 7 | LocalIP net.IP 8 | LocalPort int 9 | RemoteIP net.IP 10 | RemotePort int 11 | } 12 | -------------------------------------------------------------------------------- /go/app_interface/event.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | type Event struct { 4 | Name string 5 | Target string 6 | Data string 7 | } 8 | -------------------------------------------------------------------------------- /go/app_interface/interface.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type AppInterface interface { 8 | CallFunction(string, []byte) ([]byte, error) 9 | } 10 | 11 | type AppFunctions struct { 12 | javaInterface AppInterface 13 | } 14 | 15 | func (s *AppFunctions) call(functionName string, args []byte) ([]byte, error) { 16 | if s == nil { 17 | return nil, fmt.Errorf("reference was nil") 18 | } 19 | return s.javaInterface.CallFunction(functionName, args) 20 | } 21 | -------------------------------------------------------------------------------- /go/app_interface/network_address.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type NetworkAddress struct { 9 | Addr string 10 | PrefixLength int 11 | IsIPv6 bool 12 | } 13 | 14 | func (a *NetworkAddress) ToIPNet() (*net.IPNet, error) { 15 | ip := net.ParseIP(a.Addr) 16 | if ip == nil { 17 | return nil, fmt.Errorf("failed to parse ip: %s", a.Addr) 18 | } 19 | 20 | var mask net.IPMask 21 | if a.IsIPv6 { 22 | mask = net.CIDRMask(a.PrefixLength, net.IPv6len) 23 | } else { 24 | mask = net.CIDRMask(a.PrefixLength, net.IPv4len) 25 | } 26 | ipNet := &net.IPNet{IP: ip, Mask: mask} 27 | return ipNet, nil 28 | } 29 | 30 | func (a *NetworkAddress) String() string { 31 | return a.Addr 32 | } 33 | -------------------------------------------------------------------------------- /go/app_interface/network_interface.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "net" 5 | 6 | "gvisor.dev/gvisor/pkg/tcpip" 7 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" 8 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" 9 | ) 10 | 11 | type NetworkInterface struct { 12 | Name string 13 | Index int 14 | MTU int 15 | Up bool 16 | Multicast bool 17 | Loopback bool 18 | P2P bool 19 | Addresses []NetworkAddress 20 | 21 | Flags net.Flags 22 | } 23 | 24 | func (i *NetworkInterface) setFlagsValue() { 25 | if i.Up { 26 | i.Flags |= net.FlagUp 27 | } 28 | if i.Loopback { 29 | i.Flags |= net.FlagLoopback 30 | } 31 | if i.P2P { 32 | i.Flags |= net.FlagPointToPoint 33 | } 34 | if i.Multicast { 35 | i.Flags |= net.FlagMulticast 36 | i.Flags |= net.FlagBroadcast 37 | } 38 | } 39 | 40 | func (i *NetworkInterface) GetProtocolAddresses() []tcpip.ProtocolAddress { 41 | var addresses []tcpip.ProtocolAddress 42 | for _, a := range i.Addresses { 43 | 44 | protocolAddress := tcpip.ProtocolAddress{ 45 | AddressWithPrefix: tcpip.AddressWithPrefix{ 46 | Address: tcpip.Address(a.Addr), 47 | PrefixLen: a.PrefixLength, 48 | }, 49 | } 50 | if a.IsIPv6 { 51 | protocolAddress.Protocol = ipv6.ProtocolNumber 52 | } else { 53 | protocolAddress.Protocol = ipv4.ProtocolNumber 54 | } 55 | addresses = append(addresses, protocolAddress) 56 | } 57 | return addresses 58 | } 59 | 60 | func (i *NetworkInterface) Addrs() ([]NetworkAddress, error) { 61 | return i.Addresses, nil 62 | } 63 | -------------------------------------------------------------------------------- /go/app_interface/notification.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "github.com/fxamacker/cbor/v2" 5 | "github.com/safing/portbase/log" 6 | ) 7 | 8 | type Notification struct { 9 | ID int32 10 | Title string 11 | Message string 12 | } 13 | 14 | func ShowNotification(notification *Notification) error { 15 | argsBytes, err := cbor.Marshal(notification) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | if HasServiceFunctions() { 21 | _, err = serviceFunctions.call("ShowNotification", argsBytes) 22 | } else if HasActivityFunctions() { 23 | _, err = activityFunctions.call("ShowNotification", argsBytes) 24 | } 25 | 26 | if err != nil { 27 | log.Warningf("app_interface: failed to show notification: %s", err) 28 | } 29 | return err 30 | } 31 | 32 | func CancelNotification(ID int32) error { 33 | argsBytes, err := cbor.Marshal(ID) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if HasServiceFunctions() { 39 | _, err = serviceFunctions.call("CancelNotification", argsBytes) 40 | } else if HasActivityFunctions() { 41 | _, err = activityFunctions.call("CancelNotification", argsBytes) 42 | } 43 | if err != nil { 44 | log.Warningf("app_interface: failed to cancel notification: %s", err) 45 | } 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /go/app_interface/os.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fxamacker/cbor" 7 | ) 8 | 9 | var osFunctions *AppFunctions = nil 10 | 11 | func SetOSFunctions(appInterface AppInterface) { 12 | osFunctions = &AppFunctions{javaInterface: appInterface} 13 | } 14 | 15 | func HasOSFunctions() bool { 16 | return osFunctions != nil 17 | } 18 | 19 | func GetNetworkInterfaces() ([]NetworkInterface, error) { 20 | bytes, err := osFunctions.call("GetNetworkInterfaces", nil) 21 | if err != nil { 22 | return nil, err 23 | } 24 | var interfaces []NetworkInterface 25 | err = cbor.Unmarshal(bytes, &interfaces) 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to parse response from java: %s", err) 28 | } 29 | for _, i := range interfaces { 30 | i.setFlagsValue() 31 | } 32 | return interfaces, nil 33 | } 34 | 35 | func GetNetworkAddresses() ([]NetworkAddress, error) { 36 | bytes, err := osFunctions.call("GetNetworkAddresses", nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | var addresses []NetworkAddress 41 | err = cbor.Unmarshal(bytes, &addresses) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to parse response from java: %s", err) 44 | } 45 | return addresses, nil 46 | } 47 | 48 | func GetPlatformInfo() (*PlatformInfo, error) { 49 | info := &PlatformInfo{} 50 | 51 | bytes, err := osFunctions.call("GetPlatformInfo", nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | err = cbor.Unmarshal(bytes, info) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to parse cbor: %s", err) 58 | } 59 | 60 | return info, nil 61 | } 62 | 63 | func Shutdown() error { 64 | _, err := osFunctions.call("Shutdown", nil) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /go/app_interface/platform_info.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | type PlatformInfo struct { 4 | Model string 5 | Manufacturer string 6 | Brand string 7 | Board string 8 | SDK int 9 | VersionCode int 10 | VersionName string 11 | ApplicationID string 12 | BuildType string 13 | } 14 | -------------------------------------------------------------------------------- /go/app_interface/service.go: -------------------------------------------------------------------------------- 1 | package app_interface 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fxamacker/cbor/v2" 7 | ) 8 | 9 | var serviceFunctions *AppFunctions = nil 10 | 11 | func SetServiceFunctions(appInterface AppInterface) { 12 | serviceFunctions = &AppFunctions{javaInterface: appInterface} 13 | } 14 | 15 | func HasServiceFunctions() bool { 16 | return serviceFunctions != nil 17 | } 18 | 19 | func RemoveServiceFunctionReference() { 20 | serviceFunctions = nil 21 | } 22 | 23 | func SetDefaultInterfaceForSocket(socketID uintptr) error { 24 | if serviceFunctions == nil { 25 | return fmt.Errorf("service not initialized") 26 | } 27 | 28 | args, _ := cbor.Marshal(int(socketID)) 29 | 30 | _, err := serviceFunctions.call("IgnoreSocket", args) 31 | if err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | func GetConnectionOwner(connection Connection) (int, error) { 38 | if serviceFunctions == nil { 39 | return 0, fmt.Errorf("service not initialized") 40 | } 41 | 42 | args, _ := cbor.Marshal(connection) 43 | 44 | data, err := serviceFunctions.call("GetConnectionOwner", args) 45 | if err != nil { 46 | return 0, err 47 | } 48 | var uid int 49 | err = cbor.Unmarshal(data, &uid) 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | return uid, nil 55 | } 56 | 57 | func VPNInit() (int, error) { 58 | if serviceFunctions == nil { 59 | return 0, fmt.Errorf("service not initialized") 60 | } 61 | 62 | data, err := serviceFunctions.call("VPNInit", nil) 63 | if err != nil { 64 | return 0, err 65 | } 66 | var fd int 67 | err = cbor.Unmarshal(data, &fd) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | return fd, nil 73 | } 74 | 75 | func GetAppUID() (int, error) { 76 | if serviceFunctions == nil { 77 | return 0, fmt.Errorf("service not initialized") 78 | } 79 | 80 | data, err := serviceFunctions.call("GetAppUID", nil) 81 | if err != nil { 82 | return 0, err 83 | } 84 | var uid int 85 | err = cbor.Unmarshal(data, &uid) 86 | if err != nil { 87 | return 0, err 88 | } 89 | 90 | return uid, nil 91 | } 92 | -------------------------------------------------------------------------------- /go/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # get build data 4 | if [[ "$BUILD_COMMIT" == "" ]]; then 5 | BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null) 6 | fi 7 | if [[ "$BUILD_USER" == "" ]]; then 8 | BUILD_USER=$(id -un) 9 | fi 10 | if [[ "$BUILD_HOST" == "" ]]; then 11 | BUILD_HOST=$(hostname -f) 12 | fi 13 | if [[ "$BUILD_DATE" == "" ]]; then 14 | BUILD_DATE=$(date +%d.%m.%Y) 15 | fi 16 | if [[ "$BUILD_SOURCE" == "" ]]; then 17 | BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1) 18 | fi 19 | if [[ "$BUILD_SOURCE" == "" ]]; then 20 | BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) 21 | fi 22 | BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g") 23 | 24 | # check 25 | if [[ "$BUILD_COMMIT" == "" ]]; then 26 | echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable." 27 | exit 1 28 | fi 29 | if [[ "$BUILD_USER" == "" ]]; then 30 | echo "could not automatically determine BUILD_USER, please supply manually as environment variable." 31 | exit 1 32 | fi 33 | if [[ "$BUILD_HOST" == "" ]]; then 34 | echo "could not automatically determine BUILD_HOST, please supply manually as environment variable." 35 | exit 1 36 | fi 37 | if [[ "$BUILD_DATE" == "" ]]; then 38 | echo "could not automatically determine BUILD_DATE, please supply manually as environment variable." 39 | exit 1 40 | fi 41 | if [[ "$BUILD_SOURCE" == "" ]]; then 42 | echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable." 43 | exit 1 44 | fi 45 | 46 | echo "Please notice, that this build script includes metadata into the build." 47 | echo "This information is useful for debugging and license compliance." 48 | echo "Run the compiled binary with the -version flag to see the information included." 49 | 50 | # build 51 | BUILD_PATH="github.com/safing/portbase/info" 52 | 53 | set -e 54 | echo Generating binding... 55 | cd codegen 56 | go run gen.go go.go java.go ts.go 57 | cd .. 58 | echo Building android library... 59 | gomobile bind -o ../android/app/libs/golib.aar -target=android/arm,android/arm64 -androidapi 19 -ldflags "-s -w -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" github.com/safing/portmaster-android/go/engine github.com/safing/portmaster-android/go/engine/ui/exported github.com/safing/portmaster-android/go/engine/tunnel github.com/safing/portmaster-android/go/app_interface 60 | echo Building succesiful 61 | -------------------------------------------------------------------------------- /go/codegen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "os" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | const functionsFile = "../engine/ui/functions.go" 14 | 15 | type Val struct { 16 | Name string 17 | Type string 18 | } 19 | 20 | type Func struct { 21 | Name string 22 | Params []Val 23 | ReturnTypes []string 24 | CreateProxy bool 25 | TSTypes []string 26 | } 27 | 28 | func hasPluginCallParam(f *ast.FuncDecl) bool { 29 | if f == nil || f.Type == nil { 30 | return false 31 | } 32 | 33 | // Check if the function has exactly one parameter 34 | if f.Type.Params == nil || len(f.Type.Params.List) != 1 { 35 | return false 36 | } 37 | 38 | // Check if the type of the first (and only) parameter is PluginCall 39 | return fmt.Sprintf("%s", f.Type.Params.List[0].Type) == "PluginCall" 40 | } 41 | 42 | func getNoErrorTypes(types []string) []string { 43 | var noErrorTypes []string 44 | for _, t := range types { 45 | if t != "error" { 46 | noErrorTypes = append(noErrorTypes, t) 47 | } 48 | } 49 | return noErrorTypes 50 | } 51 | 52 | var parseTSTypesRegex = regexp.MustCompile("ts:(.*)").FindString 53 | 54 | func parseTSTypes(comments []*ast.Comment) []string { 55 | for _, c := range comments { 56 | typesString := parseTSTypesRegex(c.Text) 57 | typesString, _ = strings.CutPrefix(typesString, "ts:(") 58 | typesString, _ = strings.CutSuffix(typesString, ")") 59 | typesString = strings.ReplaceAll(typesString, " ", "") 60 | return strings.Split(typesString, ",") 61 | } 62 | return nil 63 | } 64 | 65 | func main() { 66 | set := token.NewFileSet() 67 | parsedFile, err := parser.ParseFile(set, functionsFile, nil, parser.ParseComments) 68 | if err != nil { 69 | fmt.Println("Failed to parse package:", err) 70 | os.Exit(1) 71 | } 72 | 73 | funcs := []*ast.FuncDecl{} 74 | 75 | for _, d := range parsedFile.Decls { 76 | if fn, isFn := d.(*ast.FuncDecl); isFn { 77 | funcs = append(funcs, fn) 78 | } 79 | } 80 | 81 | var functions []Func 82 | for _, f := range funcs { 83 | structFunc := Func{ 84 | Name: f.Name.Name, 85 | CreateProxy: !hasPluginCallParam(f), 86 | } 87 | 88 | for _, p := range f.Type.Params.List { 89 | for _, name := range p.Names { 90 | structFunc.Params = append(structFunc.Params, Val{ 91 | Name: name.Name, 92 | Type: fmt.Sprintf("%s", p.Type), 93 | }) 94 | } 95 | } 96 | if f.Doc != nil { 97 | structFunc.TSTypes = parseTSTypes(f.Doc.List) 98 | } 99 | 100 | if f.Type.Results != nil { 101 | for _, r := range f.Type.Results.List { 102 | structFunc.ReturnTypes = append(structFunc.ReturnTypes, fmt.Sprintf("%s", r.Type)) 103 | } 104 | } 105 | 106 | functions = append(functions, structFunc) 107 | } 108 | 109 | writeToGoFile("../engine/ui/exported/proxy.go", functions) 110 | writeToJavaFile("../../android/app/src/main/java/io/safing/portmaster/android/ui/GoBridge.java", functions) 111 | writeToTSFile("../../src/app/plugins/go.bridge.ts", functions) 112 | } 113 | -------------------------------------------------------------------------------- /go/codegen/java.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "text/template" 9 | ) 10 | 11 | const javaFileTemplate = `package io.safing.portmaster.android.ui; 12 | 13 | // DO NOT EDIT THIS FILE! 14 | // The file was autogenerated by go/codegen/gen.go 15 | 16 | import com.getcapacitor.JSObject; 17 | import com.getcapacitor.Plugin; 18 | import com.getcapacitor.PluginCall; 19 | import com.getcapacitor.PluginMethod; 20 | import com.getcapacitor.annotation.CapacitorPlugin; 21 | 22 | import org.json.JSONException; 23 | 24 | @CapacitorPlugin(name = "GoBridge") 25 | public class GoBridge extends Plugin { 26 | %s 27 | public void notifyListener(String name, String data) throws JSONException { 28 | notifyListeners(name, new JSObject(data)); 29 | } 30 | } 31 | ` 32 | 33 | const javaMethodTemplate = ` 34 | @PluginMethod() 35 | public void {{.Name}}(PluginCall call) { 36 | exported.Exported.{{firstLowerCase .Name}}(new GoPluginCall(this, call)); 37 | } 38 | ` 39 | 40 | func firstLetterLowercase(name string) string { 41 | name = strings.ToLower(name[0:1]) + name[1:] 42 | return name 43 | } 44 | 45 | func writeToJavaFile(filename string, functions []Func) { 46 | javaFile, err := os.Create(filename) 47 | if err != nil { 48 | fmt.Printf("Failed to create file: %s", err) 49 | return 50 | } 51 | defer javaFile.Close() 52 | 53 | javaTmpl := template.New("JavaFunctionTemplate") 54 | javaTmpl.Funcs(template.FuncMap{ 55 | "firstLowerCase": firstLetterLowercase, 56 | }) 57 | _, err = javaTmpl.Parse(javaMethodTemplate) 58 | if err != nil { 59 | fmt.Printf("Failed to parse template: %s", err) 60 | return 61 | } 62 | 63 | buf := new(bytes.Buffer) 64 | 65 | for _, r := range functions { 66 | javaTmpl.Execute(buf, r) 67 | } 68 | 69 | _, err = javaFile.WriteString(fmt.Sprintf(javaFileTemplate, buf.String())) 70 | if err != nil { 71 | fmt.Printf("Failed to write to file: %s", err) 72 | return 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /go/engine/events.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/safing/portbase/log" 7 | "github.com/safing/portbase/modules" 8 | "github.com/safing/portmaster/netenv" 9 | "github.com/tevino/abool" 10 | ) 11 | 12 | var NET_CAPABILITY_NOT_METERED int32 = 11 13 | 14 | var ( 15 | onNotMatternNetwrokChannel chan struct{} 16 | networkChangeMutex sync.Mutex 17 | 18 | IsCurrentNetworkNotMetered abool.AtomicBool 19 | ) 20 | 21 | type Network interface { 22 | HasCapability(int32) bool 23 | } 24 | 25 | func init() { 26 | onNotMatternNetwrokChannel = make(chan struct{}) 27 | } 28 | 29 | func NotifiOnNotMeterdNetwork() <-chan struct{} { 30 | networkChangeMutex.Lock() 31 | defer networkChangeMutex.Unlock() 32 | return onNotMatternNetwrokChannel 33 | } 34 | 35 | // OnNetworkConnected called from java when new network interface is connected. 36 | func OnNetworkConnected() { 37 | // Ignore if engine is not initialized 38 | if engineInitialized.IsNotSet() { 39 | return 40 | } 41 | 42 | log.Info("engine: network interface connected") 43 | go netenv.TriggerOnlineStatusInvestigation() 44 | } 45 | 46 | // OnNetworkConnected called from java when a network interface is disconnected. 47 | func OnNetworkDisconnected() { 48 | // Ignore if engine is not initialized 49 | if engineInitialized.IsNotSet() { 50 | return 51 | } 52 | 53 | log.Info("engine: network interface disconnected") 54 | go netenv.TriggerOnlineStatusInvestigation() 55 | } 56 | 57 | // OnNetworkConnected called from java when a network interface changes some of its properties. 58 | func OnNetworkCapabilitiesChanged(network Network) { 59 | // Ignore if engine is not initialized 60 | if engineInitialized.IsNotSet() { 61 | return 62 | } 63 | 64 | // Trigger online check 65 | go netenv.TriggerOnlineStatusInvestigation() 66 | 67 | networkChangeMutex.Lock() 68 | defer networkChangeMutex.Unlock() 69 | 70 | isNotMetered := network.HasCapability(NET_CAPABILITY_NOT_METERED) 71 | if IsCurrentNetworkNotMetered.IsSet() != isNotMetered { 72 | if isNotMetered { 73 | close(onNotMatternNetwrokChannel) 74 | } else { 75 | onNotMatternNetwrokChannel = make(chan struct{}) 76 | } 77 | } 78 | IsCurrentNetworkNotMetered.SetTo(isNotMetered) 79 | } 80 | 81 | // OnIdleModeChanged is called from java when device switches from or to idle mode. 82 | func OnIdleModeChanged(isOnIdleMode bool) { 83 | log.Infof("engine: device sleep mode is enabled: %t", isOnIdleMode) 84 | modules.SetSleepMode(isOnIdleMode) 85 | } 86 | -------------------------------------------------------------------------------- /go/engine/tunnel/addr.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/safing/portmaster/network/packet" 8 | ) 9 | 10 | // Implements net.Addr 11 | type Addr struct { 12 | ip net.IP 13 | port uint16 14 | ipVersion packet.IPVersion 15 | protocol packet.IPProtocol 16 | } 17 | 18 | func (a Addr) Network() string { 19 | if a.protocol == packet.TCP { 20 | return "tcp" 21 | } 22 | if a.protocol == packet.UDP { 23 | return "udp" 24 | } 25 | 26 | return "" 27 | } 28 | 29 | func (a Addr) String() string { 30 | return fmt.Sprintf("%s:%d", a.ip, a.port) 31 | } 32 | -------------------------------------------------------------------------------- /go/engine/tunnel/default_route.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | "gvisor.dev/gvisor/pkg/tcpip" 8 | ) 9 | 10 | var ( 11 | connections []*ConnDefaultForward 12 | connMutex sync.RWMutex 13 | ) 14 | 15 | type ConnDefaultForward struct { 16 | system io.ReadWriteCloser 17 | remote io.ReadWriteCloser 18 | endpoint tcpip.Endpoint 19 | } 20 | 21 | func addDefaultConnection(system io.ReadWriteCloser, remote io.ReadWriteCloser, endpoint tcpip.Endpoint) { 22 | conn := &ConnDefaultForward{ 23 | system: system, remote: remote, endpoint: endpoint, 24 | } 25 | 26 | connMutex.Lock() 27 | defer connMutex.Unlock() 28 | connections = append(connections, conn) 29 | 30 | conn.forward() 31 | } 32 | 33 | func (c *ConnDefaultForward) forward() { 34 | go func() { 35 | errc := make(chan error, 1) 36 | go func() { 37 | _, err := io.Copy(c.remote, c.system) 38 | errc <- err 39 | }() 40 | go func() { 41 | _, err := io.Copy(c.system, c.remote) 42 | errc <- err 43 | }() 44 | <-errc 45 | onConnectionEnd(c) 46 | }() 47 | } 48 | 49 | func (c *ConnDefaultForward) close() { 50 | if c == nil { 51 | return 52 | } 53 | 54 | if c.system != nil { 55 | c.system.Close() 56 | } 57 | 58 | if c.endpoint != nil { 59 | c.endpoint.Close() 60 | } 61 | 62 | if c.remote != nil { 63 | c.remote.Close() 64 | } 65 | } 66 | 67 | // onConnectionEnd end connection callback 68 | func onConnectionEnd(conn *ConnDefaultForward) { 69 | conn.close() 70 | 71 | connMutex.Lock() 72 | defer connMutex.Unlock() 73 | for i, c := range connections { 74 | if c == conn { 75 | // replace the connection with the last in the list. A faster way of removing. 76 | connections[i] = connections[len(connections)-1] 77 | connections = connections[:len(connections)-1] 78 | return 79 | } 80 | } 81 | } 82 | 83 | func endAllDefaultConnections() { 84 | connMutex.Lock() 85 | defer connMutex.Unlock() 86 | if connections != nil { 87 | for _, c := range connections { 88 | c.close() 89 | } 90 | connections = nil 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /go/engine/tunnel/resolver.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | var httpClient *http.Client 16 | 17 | func InitializeResolver() { 18 | tr := &http.Transport{ 19 | TLSClientConfig: &tls.Config{ 20 | MinVersion: tls.VersionTLS12, 21 | ServerName: "dns.quad9.net", 22 | }, 23 | IdleConnTimeout: 3 * time.Minute, 24 | } 25 | 26 | httpClient = &http.Client{Transport: tr} 27 | } 28 | 29 | func ResolveQuery(msg []byte) ([]byte, error) { 30 | dnsQuery := new(dns.Msg) 31 | dnsQuery.Unpack(msg) 32 | 33 | // Pack query and convert to base64 string 34 | buf, err := dnsQuery.Pack() 35 | if err != nil { 36 | return nil, err 37 | } 38 | b64dns := base64.RawURLEncoding.EncodeToString(buf) 39 | 40 | // Build and execute http request 41 | url := &url.URL{ 42 | Scheme: "https", 43 | Host: "9.9.9.9", 44 | Path: "/dns-query", 45 | ForceQuery: true, 46 | RawQuery: fmt.Sprintf("dns=%s", b64dns), 47 | } 48 | 49 | request, err := http.NewRequest(http.MethodGet, url.String(), nil) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | resp, err := httpClient.Do(request) 55 | if err != nil { 56 | return nil, err 57 | } 58 | defer func() { 59 | _ = resp.Body.Close() 60 | }() 61 | 62 | if resp.StatusCode != http.StatusOK { 63 | return nil, fmt.Errorf("http request failed with %s", resp.Status) 64 | } 65 | 66 | body, err := io.ReadAll(resp.Body) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return body, nil 72 | } 73 | -------------------------------------------------------------------------------- /go/engine/tunnel/spn_route.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | 8 | "github.com/safing/portmaster/network" 9 | "github.com/safing/spn/crew" 10 | ) 11 | 12 | type ConnSPNForward struct { 13 | system io.ReadWriteCloser 14 | local Addr 15 | remote Addr 16 | } 17 | 18 | func (c ConnSPNForward) Read(b []byte) (n int, err error) { 19 | return c.system.Read(b) 20 | } 21 | 22 | func (c ConnSPNForward) Write(b []byte) (n int, err error) { 23 | return c.system.Write(b) 24 | } 25 | 26 | func (c ConnSPNForward) Close() error { 27 | return c.system.Close() 28 | } 29 | 30 | func (c ConnSPNForward) LocalAddr() net.Addr { 31 | return c.local 32 | } 33 | 34 | func (c ConnSPNForward) RemoteAddr() net.Addr { 35 | return c.remote 36 | } 37 | 38 | func (c ConnSPNForward) SetDeadline(t time.Time) error { 39 | return nil 40 | } 41 | 42 | func (c ConnSPNForward) SetReadDeadline(t time.Time) error { 43 | return nil 44 | } 45 | 46 | func (c ConnSPNForward) SetWriteDeadline(t time.Time) error { 47 | return nil 48 | } 49 | 50 | func addSPNConnection(system io.ReadWriteCloser, local Addr, remote Addr) { 51 | conn := ConnSPNForward{ 52 | system: system, 53 | local: local, 54 | remote: remote, 55 | } 56 | connInfo := network.NewDefaultConnection(local.ip, local.port, remote.ip, remote.port, local.ipVersion, local.protocol) 57 | 58 | crew.HandleSluiceRequest(connInfo, conn) 59 | } 60 | -------------------------------------------------------------------------------- /go/engine/tunnel/state.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | type TunnelState struct { 4 | Status string 5 | Error string 6 | } 7 | -------------------------------------------------------------------------------- /go/engine/ui/structs.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/safing/portbase/api" 8 | "github.com/safing/portbase/log" 9 | "github.com/safing/portmaster-android/go/engine" 10 | ) 11 | 12 | type PluginCall = engine.PluginCall 13 | 14 | var ( 15 | Database api.DatabaseAPI 16 | dbCall PluginCall = nil 17 | ) 18 | 19 | func init() { 20 | Database = api.CreateDatabaseAPI(databaseSendFunction) 21 | } 22 | 23 | type Request = struct { 24 | Method string `json:"method"` 25 | Url string `json:"url"` 26 | Headers map[string][]string `json:"headers"` 27 | Body string `json:"body"` 28 | } 29 | 30 | type ResponseWriter struct { 31 | body string 32 | statusCode int 33 | header http.Header 34 | } 35 | 36 | func NewResponseWriter() *ResponseWriter { 37 | return &ResponseWriter{ 38 | header: http.Header{}, 39 | } 40 | } 41 | 42 | func (w *ResponseWriter) Header() http.Header { 43 | return w.header 44 | } 45 | 46 | func (w *ResponseWriter) Write(b []byte) (int, error) { 47 | w.body += string(b) 48 | return len(b), nil 49 | } 50 | 51 | func (w *ResponseWriter) WriteHeader(statusCode int) { 52 | w.statusCode = statusCode 53 | } 54 | 55 | func databaseSendFunction(data []byte) { 56 | if dbCall != nil { 57 | err := dbCall.Notify("db_event", fmt.Sprintf(`{"data": %q}`, string(data))) 58 | if err != nil { 59 | log.Errorf("ui: failed to notify ui for db response: %s", err) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portmaster-android", 3 | "integrations": { 4 | "capacitor": {} 5 | }, 6 | "type": "angular" 7 | } 8 | -------------------------------------------------------------------------------- /ionic_android_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ionic capacitor copy android 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ngv'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portmaster-android", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "^16.0.2", 17 | "@angular/core": "^16.0.2", 18 | "@angular/forms": "^16.0.2", 19 | "@angular/platform-browser": "^16.0.2", 20 | "@angular/platform-browser-dynamic": "^16.0.2", 21 | "@angular/router": "^16.0.2", 22 | "@capacitor/android": "4.4.0", 23 | "@capacitor/app": "4.1.0", 24 | "@capacitor/core": "4.4.0", 25 | "@capacitor/haptics": "4.0.1", 26 | "@capacitor/keyboard": "4.0.1", 27 | "@capacitor/status-bar": "4.0.1", 28 | "@ionic/angular": "^6.1.9", 29 | "ionicons": "^6.0.3", 30 | "marked": "^4.3.0", 31 | "ngx-markdown": "^16.0.0", 32 | "rxjs": "~6.6.0", 33 | "tslib": "^2.2.0", 34 | "zone.js": "~0.13.0" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "^16.0.2", 38 | "@angular-eslint/builder": "~13.0.1", 39 | "@angular-eslint/eslint-plugin": "~13.0.1", 40 | "@angular-eslint/eslint-plugin-template": "~13.0.1", 41 | "@angular-eslint/template-parser": "~13.0.1", 42 | "@angular/cli": "^16.0.2", 43 | "@angular/compiler": "^16.0.2", 44 | "@angular/compiler-cli": "^16.0.2", 45 | "@angular/language-service": "^16.0.2", 46 | "@capacitor/cli": "4.4.0", 47 | "@ionic/angular-toolkit": "^6.0.0", 48 | "@ionic/lab": "3.2.15", 49 | "@types/jasmine": "~3.6.0", 50 | "@types/jasminewd2": "~2.0.3", 51 | "@types/marked": "^4.3.1", 52 | "@types/node": "^12.11.1", 53 | "@typescript-eslint/eslint-plugin": "5.3.0", 54 | "@typescript-eslint/parser": "5.3.0", 55 | "eslint": "^7.6.0", 56 | "eslint-plugin-import": "2.22.1", 57 | "eslint-plugin-jsdoc": "30.7.6", 58 | "eslint-plugin-prefer-arrow": "1.2.2", 59 | "jasmine-core": "~3.8.0", 60 | "jasmine-spec-reporter": "~5.0.0", 61 | "karma": "~6.3.2", 62 | "karma-chrome-launcher": "~3.1.0", 63 | "karma-coverage": "~2.0.3", 64 | "karma-coverage-istanbul-reporter": "~3.0.2", 65 | "karma-jasmine": "~4.0.0", 66 | "karma-jasmine-html-reporter": "^1.5.0", 67 | "protractor": "~7.0.0", 68 | "ts-node": "~8.3.0", 69 | "typescript": "~4.9.5" 70 | }, 71 | "description": "An Ionic project" 72 | } -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | loadChildren: () => import('./tabs/tabs.routes').then((m) => m.routes), 7 | }, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { provideRouter } from '@angular/router'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [AppComponent], 9 | providers: [provideRouter([])], 10 | }).compileComponents(); 11 | }); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EnvironmentInjector, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; 2 | import { IonicModule, Platform } from '@ionic/angular'; 3 | import { CommonModule, LocationStrategy } from '@angular/common'; 4 | 5 | import JavaBridge from './plugins/java.bridge'; 6 | import { Router } from '@angular/router'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: 'app.component.html', 11 | styleUrls: ['app.component.scss'], 12 | standalone: true, 13 | imports: [IonicModule, CommonModule], 14 | }) 15 | export class AppComponent implements OnInit, OnDestroy { 16 | 17 | public environmentInjector = inject(EnvironmentInjector); 18 | 19 | constructor(private platform: Platform, private router: Router, private locationStrategy: LocationStrategy) {} 20 | 21 | async ngOnInit(): Promise { 22 | var welcomeScreen = await JavaBridge.shouldShowWelcomeScreen(); 23 | 24 | if(welcomeScreen.show) { 25 | this.router.navigate(["/welcome"]); 26 | } 27 | 28 | this.platform.backButton.subscribeWithPriority(10, () => { 29 | this.locationStrategy.back() 30 | }); 31 | } 32 | 33 | ngOnDestroy(): void {} 34 | } 35 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | loadChildren: () => import('./tabs/tabs.routes').then((m) => m.routes), 7 | }, 8 | { 9 | path: 'welcome', 10 | loadComponent: () => import('./welcome/welcome.component').then((m) => m.WelcomeComponent), 11 | }, 12 | { 13 | path: 'login', 14 | loadComponent: () => 15 | import('./login/login.component').then((m) => m.LoginComponent), 16 | }, 17 | { 18 | path: 'menu/bug-report', 19 | loadComponent: () => import('./menu/bug-report/bug-report.component').then((m) => m.BugReportComponent), 20 | }, 21 | { 22 | path: 'menu/enabled-apps', 23 | loadComponent: () => import('./menu/enabled-apps/enabled-apps.component').then((m) => m.EnabledAppsComponent), 24 | }, 25 | { 26 | path: 'menu/logs', 27 | loadComponent: () => import('./menu/logs/logs.component').then((m) => m.LogsComponent), 28 | }, 29 | { 30 | path: 'menu/user-info', 31 | loadComponent: () => import('./menu/user-info/user-info.component').then((m) => m.UserInfoComponent), 32 | }, 33 | { 34 | path: 'menu/vpn-settings', 35 | loadComponent: () => import('./menu/vpn-settings/vpn-settings.component').then((m) => m.VpnSettingsComponent), 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /src/app/help/help.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Setup Auto-Start and Always-On

4 | 5 |
6 |
7 |

🐞 Report a Bug

8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | Advanced Settings 16 | 17 |
18 | 19 | Apps Settings 20 | 21 | 22 | Logs 23 | 24 | 25 | Export Debug Info 26 | 27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /src/app/help/help.component.scss: -------------------------------------------------------------------------------- 1 | .help-section { 2 | background: var(--ion-color-pm-accent); 3 | width: 96%; 4 | height: 15%; 5 | padding: 0.5rem; 6 | margin: 2% 2% 2% 2%; 7 | border-radius: 0.25rem; 8 | } 9 | 10 | .secondary-text { 11 | font-size: .75rem; 12 | line-height: 1rem; 13 | font-weight: 500; 14 | color: rgb(171 171 171); 15 | } -------------------------------------------------------------------------------- /src/app/help/help.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | import { CommonModule, LocationStrategy } from '@angular/common'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { Router } from '@angular/router'; 7 | import GoBridge from '../plugins/go.bridge'; 8 | 9 | 10 | @Component({ 11 | selector: 'app-help', 12 | templateUrl: './help.component.html', 13 | styleUrls: ['./help.component.scss'], 14 | standalone: true, 15 | imports: [CommonModule, FormsModule, IonicModule] 16 | }) 17 | export class HelpComponent { 18 | 19 | constructor(private router: Router) {} 20 | 21 | openBugReport() { 22 | this.router.navigate(["/menu/bug-report"]); 23 | } 24 | 25 | openVPNSettings() { 26 | this.router.navigate(["/menu/vpn-settings"]); 27 | } 28 | 29 | openLogs() { 30 | this.router.navigate(["/menu/logs"]); 31 | } 32 | 33 | openEnabledApps() { 34 | this.router.navigate(["/menu/enabled-apps"]); 35 | } 36 | 37 | exportDebugInfo() { 38 | GoBridge.GetDebugInfoFile(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/lib/core.types.ts: -------------------------------------------------------------------------------- 1 | import { TrackByFunction } from '@angular/core'; 2 | 3 | export enum SecurityLevel { 4 | Off = 0, 5 | Normal = 1, 6 | High = 2, 7 | Extreme = 4, 8 | } 9 | 10 | export enum RiskLevel { 11 | Off = 'off', 12 | Auto = 'auto', 13 | Low = 'low', 14 | Medium = 'medium', 15 | High = 'high' 16 | } 17 | 18 | /** Interface capturing any object that has an ID member. */ 19 | export interface Identifyable { 20 | ID: string | number; 21 | } 22 | 23 | /** A TrackByFunction for all Identifyable objects. */ 24 | export const trackById: TrackByFunction = (_: number, obj: Identifyable) => { 25 | return obj.ID; 26 | } 27 | 28 | export function getEnumKey(enumLike: any, value: string | number): string { 29 | if (typeof value === 'string') { 30 | return value.toLowerCase() 31 | } 32 | 33 | return (enumLike[value] as string).toLowerCase() 34 | } 35 | -------------------------------------------------------------------------------- /src/app/lib/debug-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Inject, Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { PORTMASTER_HTTP_API_ENDPOINT } from './portapi.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class DebugAPI { 10 | constructor( 11 | private http: HttpClient, 12 | @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string, 13 | ) { } 14 | 15 | ping(): Observable { 16 | return this.http.get(`${this.httpAPI}/v1/ping`, { 17 | responseType: 'text' 18 | }) 19 | } 20 | 21 | getStack(): Observable { 22 | return this.http.get(`${this.httpAPI}/v1/debug/stack`, { 23 | responseType: 'text' 24 | }) 25 | } 26 | 27 | getDebugInfo(style = 'github'): Observable { 28 | return this.http.get(`${this.httpAPI}/v1/debug/info`, { 29 | params: { 30 | style, 31 | }, 32 | responseType: 'text', 33 | }) 34 | } 35 | 36 | getCoreDebugInfo(style = 'github'): Observable { 37 | return this.http.get(`${this.httpAPI}/v1/debug/core`, { 38 | params: { 39 | style, 40 | }, 41 | responseType: 'text', 42 | }) 43 | } 44 | 45 | getProfileDebugInfo(source: string, id: string, style = 'github'): Observable { 46 | return this.http.get(`${this.httpAPI}/v1/debug/network`, { 47 | params: { 48 | profile: `${source}/${id}`, 49 | style, 50 | }, 51 | responseType: 'text', 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/lib/module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from "@angular/core"; 2 | import { AppProfileService } from "./app-profile.service"; 3 | import { ConfigService } from "./config.service"; 4 | import { DebugAPI } from "./debug-api.service"; 5 | import { MetaAPI } from "./meta-api.service"; 6 | import { Netquery } from "./netquery.service"; 7 | import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service"; 8 | import { SPNService } from "./spn.service"; 9 | import { WebsocketService } from "./websocket.service"; 10 | 11 | export interface ModuleConfig { 12 | httpAPI?: string; 13 | websocketAPI?: string; 14 | } 15 | 16 | @NgModule({}) 17 | export class PortmasterAPIModule { 18 | 19 | /** 20 | * Configures a module with additional providers. 21 | * 22 | * @param cfg The module configuration defining the Portmaster HTTP and Websocket API endpoints. 23 | */ 24 | static forRoot(cfg: ModuleConfig = {}): ModuleWithProviders { 25 | if (cfg.httpAPI === undefined) { 26 | cfg.httpAPI = `http://${window.location.host}/api`; 27 | } 28 | if (cfg.websocketAPI === undefined) { 29 | cfg.websocketAPI = `ws://${window.location.host}/api/database/v1`; 30 | } 31 | 32 | return { 33 | ngModule: PortmasterAPIModule, 34 | providers: [ 35 | PortapiService, 36 | WebsocketService, 37 | MetaAPI, 38 | ConfigService, 39 | AppProfileService, 40 | DebugAPI, 41 | Netquery, 42 | SPNService, 43 | { 44 | provide: PORTMASTER_HTTP_API_ENDPOINT, 45 | useValue: cfg.httpAPI, 46 | }, 47 | { 48 | provide: PORTMASTER_WS_API_ENDPOINT, 49 | useValue: cfg.websocketAPI 50 | } 51 | ] 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/app/lib/spn.types.ts: -------------------------------------------------------------------------------- 1 | import { GeoCoordinates, IntelEntity } from './network.types'; 2 | import { Record } from './portapi.types'; 3 | 4 | export interface SPNStatus extends Record { 5 | Status: 'failed' | 'disabled' | 'connecting' | 'connected'; 6 | HomeHubName: string; 7 | HomeHubID: string; 8 | ConnectedIP: string; 9 | ConnectedTransport: string; 10 | ConnectedSince: string | null; 11 | } 12 | 13 | export interface Pin extends Record { 14 | ID: string; 15 | Name: string; 16 | FirstSeen: string; 17 | EntityV4?: IntelEntity | null; 18 | EntityV6?: IntelEntity | null; 19 | States: string[]; 20 | SessionActive: boolean; 21 | HopDistance: number; 22 | ConnectedTo: { 23 | [key: string]: Lane, 24 | }; 25 | Route: string[] | null; 26 | VerifiedOwner: string; 27 | } 28 | 29 | export interface Lane { 30 | HubID: string; 31 | Capacity: number; 32 | Latency: number; 33 | } 34 | 35 | export function getPinCoords(p: Pin): GeoCoordinates | null { 36 | if (p.EntityV4 && p.EntityV4.Coordinates) { 37 | return p.EntityV4.Coordinates; 38 | } 39 | return p.EntityV6?.Coordinates || null; 40 | } 41 | 42 | export interface Device { 43 | name: string; 44 | id: string; 45 | } 46 | 47 | export interface Subscription { 48 | ends_at: string; 49 | state: string; 50 | } 51 | 52 | export interface Plan { 53 | name: string; 54 | amount: number; 55 | months: number; 56 | renewable: boolean; 57 | feature_ids: string[]; 58 | } 59 | 60 | export interface View { 61 | Message : string; 62 | ShowAccountData : boolean; 63 | ShowAccountButton : boolean; 64 | ShowLoginButton : boolean; 65 | ShowRefreshButton : boolean; 66 | ShowLogoutButton : boolean; 67 | } 68 | 69 | export interface UserProfile extends Record { 70 | username: string; 71 | state: string; 72 | balance: number; 73 | device: Device | null; 74 | subscription: Subscription | null; 75 | current_plan: Plan | null; 76 | next_plan: Plan | null; 77 | view: View | null; 78 | LastNotifiedOfEnd?: string; 79 | LoggedInAt?: string; 80 | } 81 | -------------------------------------------------------------------------------- /src/app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function deepClone(o: T | null): T { 3 | if (o === null) { 4 | return null as any as T; 5 | } 6 | 7 | let _out: T = (Array.isArray(o) ? [] : {}) as any; 8 | for (let _key in o) { 9 | let v = o[_key]; 10 | _out[_key] = (typeof v === "object") ? deepClone(v) : v; 11 | } 12 | return _out as T; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket'; 3 | 4 | @Injectable() 5 | export class WebsocketService { 6 | constructor() { } 7 | 8 | /** 9 | * createConnection creates a new websocket connection using opts. 10 | * 11 | * @param opts Options for the websocket connection. 12 | */ 13 | createConnection(opts: WebSocketSubjectConfig): WebSocketSubject { 14 | return webSocket(opts); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Username: 4 | 5 | 6 | 7 | Password: 8 | 9 | 10 | 11 | 12 | 13 | 14 | Login 15 | 16 | Don't have an account?
Sign Up
17 |
18 | 19 | {{Error}} 20 | 21 |
-------------------------------------------------------------------------------- /src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .error{ 2 | --min-height: 2rem; 3 | margin: 0px 2px 0px 2px; 4 | --background: #00000000; 5 | --color: #ff3c22; 6 | --highlight-height: 0px; 7 | --border-style: none; 8 | } 9 | 10 | .error_label { 11 | margin: 3px 0px 3px 0; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | import { CommonModule, LocationStrategy } from '@angular/common'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { SPNService } from '../lib/spn.service'; 7 | // import { SPNService } from '@safing/portmaster-api/src/lib/spn.service'; 8 | 9 | 10 | @Component({ 11 | selector: 'app-login-container', 12 | templateUrl: './login.component.html', 13 | styleUrls: ['./login.component.scss'], 14 | standalone: true, 15 | imports: [CommonModule, FormsModule, IonicModule] 16 | }) 17 | export class LoginComponent { 18 | Error: string; 19 | 20 | Username: string 21 | Password: string 22 | 23 | ShowPassword: boolean 24 | PasswordFieldType: "password" | "text"; 25 | 26 | constructor( 27 | private spnService: SPNService, 28 | private location: LocationStrategy) { 29 | this.PasswordFieldType = "password"; 30 | } 31 | 32 | login() { 33 | this.spnService.login({username: this.Username, password: this.Password}) 34 | .subscribe( 35 | _ => { 36 | this.location.back(); 37 | }, 38 | err => { 39 | this.Error = err; 40 | }, 41 | ); 42 | } 43 | 44 | async togglePasswordVisibility(): Promise { 45 | this.ShowPassword = !this.ShowPassword; 46 | if(this.ShowPassword) { 47 | this.PasswordFieldType = "text"; 48 | } else { 49 | this.PasswordFieldType = "password"; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/menu/bug-report/bug-report.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Title 4 | 5 | 6 | 7 | 8 | 9 | What happened? 10 | 11 | 12 | 13 | 14 | 15 | What did you expect to happen? 16 | 17 | 18 | 19 | 20 | 21 | How did you reproduce it? 22 | 23 | 24 | 25 | 26 | 27 | Additional information 28 | 29 | 30 | 31 | 32 | 33 | 34 | Include debug info 35 | 36 |
37 | 38 | 39 |

The following debug information will be sent together with your report. Please check it and remove potentially sensitive information. The debug information sent with your reports will be saved on Safing's self-hosted pastebin server and is viewable via its created url. The data is automatically destroyed after one month.

40 |
41 |
42 | 43 | 44 | 45 | Included Debug Info 46 | 47 |
48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 | Create on github 56 | Send private ticket 57 |
58 | -------------------------------------------------------------------------------- /src/app/menu/bug-report/bug-report.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/app/menu/bug-report/bug-report.component.scss -------------------------------------------------------------------------------- /src/app/menu/enabled-apps/application.ts: -------------------------------------------------------------------------------- 1 | export class Application { 2 | packageName: string 3 | name: string 4 | enabled: boolean 5 | system: boolean 6 | } -------------------------------------------------------------------------------- /src/app/menu/enabled-apps/enabled-apps.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cancel 5 | 6 | Allowed apps 7 | 8 | Save 9 | 10 | 11 | 12 | 13 | 14 | 15 | Show system apps: 16 | 17 | 18 |

Disable the apps that you don't want to be routed through the SPN.

19 |
20 | 21 | 22 | {{app.name}} 23 | 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /src/app/menu/enabled-apps/enabled-apps.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/app/menu/enabled-apps/enabled-apps.component.scss -------------------------------------------------------------------------------- /src/app/menu/enabled-apps/enabled-apps.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | import { EnabledAppsComponent } from './enabled-apps.component'; 5 | 6 | describe('EnabledAppsComponent', () => { 7 | let component: EnabledAppsComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ EnabledAppsComponent ], 13 | imports: [IonicModule.forRoot()] 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(EnabledAppsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | })); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/menu/enabled-apps/enabled-apps.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { IonicModule } from '@ionic/angular'; 4 | import GoBridge from '../../plugins/go.bridge'; 5 | import JavaBridge from '../../plugins/java.bridge'; 6 | 7 | import { Application } from './application'; 8 | import { SystemAppList } from './enabled-apps.filter'; 9 | import { CommonModule, LocationStrategy } from '@angular/common'; 10 | import { FormsModule } from '@angular/forms'; 11 | 12 | @Component({ 13 | selector: 'app-enabled-apps', 14 | templateUrl: './enabled-apps.component.html', 15 | styleUrls: ['./enabled-apps.component.scss'], 16 | standalone: true, 17 | imports: [CommonModule, IonicModule, FormsModule, SystemAppList] 18 | }) 19 | export class EnabledAppsComponent implements OnInit { 20 | AppList: Application[]; 21 | 22 | ShowSystemApps: boolean = false; 23 | 24 | constructor(private locationStrategy: LocationStrategy) {} 25 | 26 | ngOnInit() { 27 | JavaBridge.getAppSettings().then((result) => { 28 | this.AppList = result.apps; 29 | this.AppList.sort((a, b) => a.name.localeCompare(b.name)); 30 | }); 31 | } 32 | 33 | Save() { 34 | var packageNameList: string[] = []; 35 | this.AppList.forEach(element => { 36 | if(!element.enabled) { 37 | packageNameList.push(element.packageName); 38 | } 39 | }); 40 | 41 | JavaBridge.setAppSettings({apps: packageNameList}); 42 | GoBridge.RestartTunnel(); 43 | this.locationStrategy.back() 44 | } 45 | 46 | Close() { 47 | this.locationStrategy.back() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/menu/enabled-apps/enabled-apps.filter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | import { Application } from './application'; 4 | 5 | @Pipe({ 6 | name: 'systemAppsFilter', 7 | standalone: true, 8 | }) 9 | export class SystemAppList implements PipeTransform { 10 | transform(apps: Application[], filterSystemApps: boolean): any { 11 | if(!apps) { 12 | return null; 13 | } 14 | 15 | if (filterSystemApps) { 16 | return apps.filter(app => !app.system); 17 | } else { 18 | return apps; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/menu/logs/logs.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Logs 8 | 9 | 10 | 11 | 16 | 17 |
18 |
19 |
{{line.Meta}}
20 |
{{line.Severity}}:{{line.Content}}
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app/menu/logs/logs.component.scss: -------------------------------------------------------------------------------- 1 | div { 2 | font-size: small; 3 | } 4 | 5 | .logline { 6 | margin: 1%; 7 | } 8 | 9 | .info { 10 | color: white; 11 | } 12 | 13 | .warning { 14 | color:goldenrod; 15 | } 16 | 17 | .error { 18 | color: red; 19 | } 20 | .critical { 21 | color: plum 22 | } 23 | hr { 24 | border: 0; 25 | border-top: 1pt solid lightgray; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/menu/logs/logs.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; 2 | import GoBridge from '../../plugins/go.bridge'; 3 | import { CommonModule, LocationStrategy, NgTemplateOutlet } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { IonContent, IonInfiniteScrollContent, IonicModule } from '@ionic/angular'; 6 | 7 | class LogLine { 8 | Meta: string 9 | Content: string 10 | Severity: string 11 | ID: number 12 | } 13 | 14 | @Component({ 15 | selector: 'app-logs', 16 | standalone: true, 17 | templateUrl: './logs.component.html', 18 | styleUrls: ['./logs.component.scss'], 19 | imports: [CommonModule, FormsModule, IonicModule] 20 | }) 21 | export class LogsComponent implements OnInit, OnDestroy { 22 | private Timer: any; 23 | 24 | @ViewChild('content') content : IonContent; 25 | Logs: LogLine[]; 26 | 27 | constructor(private location: LocationStrategy) {} 28 | 29 | ngOnInit(): void { 30 | // Request the full log buffer. 31 | GoBridge.GetLogs(0).then((logs: any) => { 32 | this.Logs = logs; 33 | 34 | if(this.content != undefined) { 35 | this.content.scrollToBottom(); 36 | } 37 | this.logUpdater(); 38 | }); 39 | } 40 | 41 | ngOnDestroy(): void { 42 | clearInterval(this.Timer); 43 | } 44 | 45 | public logUpdater() { 46 | this.Timer = setInterval(async () => { 47 | var ID = 0; 48 | if (this.Logs != null && this.Logs.length > 0) { 49 | ID = this.Logs[this.Logs.length - 1].ID; 50 | } 51 | 52 | // Request updates of the log buffer 53 | var logs = await GoBridge.GetLogs(ID); 54 | if (logs != null) { 55 | this.Logs = this.Logs.concat(logs); 56 | if (logs.length > 0) { 57 | this.content.scrollToBottom(200); 58 | } 59 | } 60 | }, 1000) 61 | } 62 | 63 | Close() { 64 | this.location.back() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.scss: -------------------------------------------------------------------------------- 1 | .header-row { 2 | background: #7163AA; 3 | color: #fff; 4 | font-size: 18px; 5 | } 6 | 7 | ion-col { 8 | border: 1px solid #ECEEEF; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EnvironmentInjector, EventEmitter, Input, OnInit, Output, ViewChild, inject } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { FormsModule } from '@angular/forms'; 5 | 6 | import { Router, RouterModule } from '@angular/router'; 7 | 8 | @Component({ 9 | selector: 'app-menu', 10 | templateUrl: './menu.component.html', 11 | styleUrls: ['./menu.component.scss'], 12 | standalone: true, 13 | imports: [CommonModule, IonicModule, FormsModule ] 14 | }) 15 | export class MenuComponent { 16 | constructor(private router: Router) {} 17 | 18 | 19 | openVPNSettings(): void { 20 | this.router.navigate(["/menu/vpn-settings"]); 21 | } 22 | 23 | openBugReport(): void { 24 | this.router.navigate(["/menu/bug-report"]); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/menu/user-info/user-info.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | User Info 4 | 5 | 6 | 7 | 8 | 9 | Package 10 | {{ User?.current_plan?.name }} 11 | 12 | 13 | Package Runs Until 14 | {{ User?.subscription?.ends_at | date:'medium' }} 15 | 16 | 17 | Username 18 | {{ User?.username }} 19 | 20 | 21 | Device Name 22 | {{ User?.device?.name }} 23 | 24 | 25 | Device ID 26 | {{ User?.device?.id }} 27 | 28 | 29 | Account State 30 | {{ User?.state }} 31 | 32 | 33 | Logged in Since 34 | {{ User?.LoggedInAt | date:'medium' }} 35 | 36 | 37 | 38 | 39 | Logout 40 | 41 | 42 | Refresh 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/app/menu/user-info/user-info.component.scss: -------------------------------------------------------------------------------- 1 | .user_row:nth-child(even){ 2 | // border: 1px solid #ECEEEF; 3 | background-color: #343333; 4 | } 5 | 6 | .user_row:nth-child(odd){ 7 | // border: 1px solid #ECEEEF; 8 | background-color: #000000; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/menu/user-info/user-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CommonModule, LocationStrategy } from '@angular/common'; 3 | import { UserProfile } from 'src/app/lib/spn.types'; 4 | import { SPNService } from 'src/app/lib/spn.service'; 5 | import { IonicModule } from '@ionic/angular'; 6 | 7 | @Component({ 8 | selector: 'app-user-info', 9 | templateUrl: './user-info.component.html', 10 | styleUrls: ['./user-info.component.scss'], 11 | standalone: true, 12 | imports: [CommonModule, IonicModule] 13 | }) 14 | export class UserInfoComponent implements OnInit { 15 | 16 | User: UserProfile; 17 | 18 | constructor( 19 | private spnService: SPNService, 20 | private location: LocationStrategy) { } 21 | 22 | ngOnInit(): void { 23 | this.spnService.watchProfile().subscribe((user) => { 24 | if (user?.state !== '') { 25 | this.User = user || null; 26 | } else { 27 | this.User = null; 28 | } 29 | }); 30 | } 31 | 32 | refresh(): void { 33 | this.spnService.userProfile(true).subscribe(); 34 | } 35 | 36 | public logout(): void { 37 | this.spnService.logout(true).subscribe(() => { 38 | this.User = null; 39 | this.location.back(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/menu/vpn-settings/vpn-settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | System Settings Guide 4 | 5 | 6 | 7 |

8 | To be protected at all times, quickly make sure Portmaster is always on: 9 |

10 | 11 | 12 |
13 | 14 | 15 | 16 | 1. Tap the next to Portmaster. 17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 | 30 | 2. Enable "Always-on VPN". This makes sure Portmaster is always running, even after a restart. 31 |
32 |
33 | 34 |
35 | 36 | Optional: For further privacy, enable "Block connections without VPN". This setting prevents any connections from accessing the Internet outside of Portmaster. Even those excluded in "Apps Settings". 37 |
38 |
39 |
40 | 41 | Configure Now 42 | 43 |
44 | -------------------------------------------------------------------------------- /src/app/menu/vpn-settings/vpn-settings.component.scss: -------------------------------------------------------------------------------- 1 | .slide-image { 2 | border-radius: 7%; 3 | padding-left: 10px; 4 | padding-right: 10px; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/menu/vpn-settings/vpn-settings.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import JavaBridge from 'src/app/plugins/java.bridge'; 5 | 6 | @Component({ 7 | selector: 'app-vpn-settings', 8 | templateUrl: './vpn-settings.component.html', 9 | styleUrls: ['./vpn-settings.component.scss'], 10 | standalone: true, 11 | imports: [CommonModule, IonicModule] 12 | }) 13 | export class VpnSettingsComponent implements OnInit { 14 | 15 | constructor() {} 16 | 17 | ngOnInit() {} 18 | 19 | openSystemVPNSettings() { 20 | JavaBridge.openVPNSettings(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/plugins/java.bridge.ts: -------------------------------------------------------------------------------- 1 | import { registerPlugin } from '@capacitor/core'; 2 | 3 | export interface JavaBridgeInterface { 4 | getAppSettings(): Promise; 5 | setAppSettings(apps: any): Promise; 6 | requestVPNPermission(): Promise; 7 | isVPNPermissionGranted(): Promise; 8 | requestNotificationsPermission(): Promise; 9 | isNotificationPermissionGranted(): Promise; 10 | initEngine(): Promise; 11 | shouldShowWelcomeScreen(): Promise; 12 | openUrlInBrowser(data: {url: string}): Promise; 13 | setWelcomeScreenShowed(data: {showed: boolean}): Promise; 14 | openVPNSettings(): Promise; 15 | 16 | // Default plugin functions. 17 | addListener(eventId, listener): Promise; 18 | } 19 | const JavaBridge = registerPlugin("JavaBridge") 20 | export default JavaBridge; -------------------------------------------------------------------------------- /src/app/services/http.backend.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { Injectable } from '@angular/core'; 10 | import { Observable } from 'rxjs'; 11 | 12 | 13 | // import {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpJsonParseError, HttpResponse, HttpStatusCode, HttpUploadProgressEvent} from './response'; 14 | import { HttpBackend, HttpEvent, HttpRequest, HttpResponse } from '@angular/common/http'; 15 | import GoBridge from '../plugins/go.bridge'; 16 | 17 | 18 | /** 19 | * Determine an appropriate URL for the response, by checking either 20 | * XMLHttpRequest.responseURL or the X-Request-URL header. 21 | */ 22 | function getResponseUrl(xhr: any): string | null { 23 | if ('responseURL' in xhr && xhr.responseURL) { 24 | return xhr.responseURL; 25 | } 26 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { 27 | return xhr.getResponseHeader('X-Request-URL'); 28 | } 29 | return null; 30 | } 31 | 32 | /** 33 | * Uses `XMLHttpRequest` to send requests to a backend server. 34 | * @see `HttpHandler` 35 | * @see `JsonpClientBackend` 36 | * 37 | * @publicApi 38 | */ 39 | @Injectable() 40 | export class HttpGoBackend implements HttpBackend { 41 | 42 | constructor() { 43 | console.log("Initializing HttpGoBackend..."); 44 | } 45 | 46 | /** 47 | * Processes a request and returns a stream of response events. 48 | * @param req The request object. 49 | * @returns An observable of the response events. 50 | */ 51 | handle(req: HttpRequest): Observable> { 52 | // Observable that will create a fake http request to the Go library. 53 | const observable = new Observable>((subscriber) => { 54 | // Create object that Go library will able to parse. 55 | var requestJson = { 56 | url: req.urlWithParams, 57 | method: req.method, 58 | body: req.serializeBody(), 59 | headers: {}, 60 | } 61 | 62 | // Convert headers to map 63 | req.headers.keys().forEach((key: string) => { 64 | requestJson.headers[key] = req.headers.getAll(key); 65 | }); 66 | 67 | // Send request to the go library and wait for a response. 68 | GoBridge.PerformRequest({ requestJson: JSON.stringify(requestJson) }).then((body: any) => { 69 | console.log("Response body:", body.data); 70 | subscriber.next(new HttpResponse({ body: body.data })); 71 | }).catch((err: string) => { 72 | subscriber.error(err); 73 | }).finally(() => { 74 | subscriber.complete(); 75 | }) 76 | }); 77 | 78 | return observable; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/services/shutdown.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { AlertController, LoadingController } from "@ionic/angular"; 3 | import GoBridge from "../plugins/go.bridge"; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ShutdownService { 9 | 10 | constructor(private loadingCtrl: LoadingController, 11 | private alertController: AlertController,) { } 12 | 13 | public promptShutdown() { 14 | this.shutdown("Shutting Down Portmaster", "Shutting down the Portmaster will stop all Portmaster components and will leave your system unprotected!"); 15 | } 16 | 17 | public promptRestart() { 18 | this.shutdown("Shutting Down Portmaster", "Restart is requierd for the changes to take effect. You have to manually start portmaster after that."); 19 | } 20 | 21 | public shutdown(header: string, message: string) { 22 | this.alertController.create({ 23 | header: header, 24 | message: message, 25 | buttons: [ 26 | { 27 | text: "Shutdown", 28 | handler: () => { 29 | this.showShutdownOverlay(); 30 | GoBridge.Shutdown(); 31 | } 32 | }, 33 | { 34 | text: "Cancel", 35 | }] 36 | }).then((alert) => { 37 | alert.present(); 38 | }); 39 | } 40 | 41 | private showShutdownOverlay() { 42 | this.loadingCtrl.create({ 43 | message: 'Shuting down...', 44 | duration: 0, 45 | spinner: 'circular', 46 | }).then((loading) => { 47 | loading.present(); 48 | }); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/app/services/updater.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { filter, map } from 'rxjs/operators'; 4 | import { PortapiService } from '../lib/portapi.service'; 5 | import { RegistryState } from './updater.types'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class UpdaterService { 11 | 12 | readonly statusPrefix = "runtime:" 13 | readonly updatesStateQuery = this.statusPrefix + "core/updates/state" 14 | 15 | /** 16 | * status$ watches the global core status. It's mutlicasted using a BehaviorSubject so new 17 | * subscribers will automatically get the latest version while only one subscription 18 | * to the backend is held. 19 | */ 20 | constructor(private portapi: PortapiService) { } 21 | 22 | 23 | /** 24 | * Loads the current status of all subsystems matching idPrefix. 25 | * If idPrefix is an empty string all subsystems are returned. 26 | * 27 | * @param idPrefix An optional ID prefix to limit the returned subsystems 28 | */ 29 | query(): Observable { 30 | return this.portapi.query(this.updatesStateQuery) 31 | .pipe( 32 | map(reply => reply.data), 33 | ) 34 | } 35 | 36 | /** 37 | * Watches the user profile. It will emit null if there is no profile available yet. 38 | */ 39 | watchState(): Observable { 40 | let hasSent = false; 41 | return this.portapi.watch(this.updatesStateQuery, {}, { forwardDone: true }) 42 | .pipe( 43 | filter(result => { 44 | if ('type' in result && result.type === 'done') { 45 | if (hasSent) { 46 | return false; 47 | } 48 | } 49 | 50 | return true 51 | }), 52 | map(result => { 53 | hasSent = true; 54 | if ('type' in result) { 55 | return null; 56 | } 57 | 58 | return result; 59 | }) 60 | ); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/app/services/updater.types.ts: -------------------------------------------------------------------------------- 1 | import { Record } from "../lib/portapi.types"; 2 | 3 | export enum State { 4 | Ready = "ready", // Default idle state. 5 | Checking = "checking", // Downloading indexes. 6 | Downloading = "downloading", // Downloading updates. 7 | Fetching = "fetching", // Fetching a single file. 8 | } 9 | 10 | export interface StateDownloadingDetails { 11 | // Resources holds the resource IDs that are being downloaded. 12 | Resources: string[], 13 | 14 | // FinishedUpTo holds the index of Resources that is currently being 15 | // downloaded. Previous resources have finished downloading. 16 | FinishedUpTo: number 17 | } 18 | 19 | export interface UpdateState { 20 | // LastCheckAt holds the time of the last update check. 21 | LastCheckAt: string, 22 | // LastCheckError holds the error of the last check. 23 | LastCheckError: string, 24 | // PendingDownload holds the resources that are pending download. 25 | PendingDownload: string[], 26 | 27 | // LastDownloadAt holds the time when resources were downloaded the last time. 28 | LastDownloadAt: string, 29 | // LastDownloadError holds the error of the last download. 30 | LastDownloadError: string, 31 | // LastDownload holds the resources that we downloaded the last time updates 32 | // were downloaded. 33 | LastDownload: string[], 34 | 35 | // LastSuccessAt holds the time of the last successful update (check). 36 | LastSuccessAt: string 37 | } 38 | 39 | export interface RegistryState extends Record { 40 | ID: State | null, 41 | Details: StateDownloadingDetails | null, 42 | Updates: UpdateState | null, 43 | } 44 | -------------------------------------------------------------------------------- /src/app/services/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { WebSocketSubjectConfig } from 'rxjs/webSocket'; 3 | import { AnonymousSubject } from 'rxjs/internal/Subject'; 4 | import { Observable, Subscriber } from 'rxjs'; 5 | import GoBridge, { GoInterface } from '../plugins/go.bridge'; 6 | 7 | @Injectable() 8 | export class WebsocketGoService { 9 | constructor() { } 10 | 11 | /** 12 | * createConnection creates a new websocket connection using opts. 13 | * 14 | * @param opts Options for the websocket connection. 15 | */ 16 | createConnection(opts: WebSocketSubjectConfig): AnonymousSubject { 17 | opts.openObserver.next(null); 18 | 19 | let source = { 20 | next: (value: T): void => { 21 | console.log("Observer: ", opts.serializer(value)); 22 | GoBridge.DatabaseMessage(opts.serializer(value) as string); 23 | }, 24 | error: (): void => {}, 25 | complete: (): void => {}, 26 | }; 27 | 28 | let destination = new Observable((subscriber: Subscriber) => { 29 | GoInterface.addListener("db_event", (response) => { 30 | console.log("DB response: ", response.data); 31 | subscriber.next(opts.deserializer(response)); 32 | }); 33 | GoBridge.SubscribeToDatabase({}); 34 | }); 35 | 36 | return new AnonymousSubject(source, destination); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/app/settings/setting/config-settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Shutdown and start the app again to apply the changes. 6 |

7 |
8 |
9 | 10 | 11 | 12 | {{subsys.Name}} 13 | 14 | 15 | 16 | {{cat.name}} 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/app/settings/setting/config-settings.scss: -------------------------------------------------------------------------------- 1 | .category { 2 | --ion-background-color: #fff; 3 | 4 | } -------------------------------------------------------------------------------- /src/app/settings/setting/edit/edit.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cancel 5 | 6 | {{title}} 7 | 8 | Add 9 | Update 10 | 11 | 12 | 13 | 14 | 15 | Quick Settings 16 | 17 | 18 | 19 | {{symbolMap["-"]}} 20 | 21 | 22 | 23 | {{symbolMap["+"]}} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Delete 32 | 33 | 34 |

35 | 36 |

37 |
38 | 39 | 40 | 41 | 42 | {{setting.Name}} 44 | 45 | 46 | 47 | 48 |
-------------------------------------------------------------------------------- /src/app/settings/setting/edit/edit.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/app/settings/setting/edit/edit.component.scss -------------------------------------------------------------------------------- /src/app/settings/setting/generic-setting/generic-setting.scss: -------------------------------------------------------------------------------- 1 | .text-input { 2 | background-color: var(--ion-color-pm-accent); 3 | margin-left: 1em; 4 | } 5 | 6 | .add-rule-button { 7 | --background: #00000000; 8 | --border-radius: 4em; 9 | --border-style: dashed; 10 | --border-color: var(--ion-color-pm-accent); 11 | width: 99%; 12 | } 13 | 14 | .description { 15 | padding: 1em 1em 1em 1em; 16 | } 17 | 18 | .not-default { 19 | border-left: 0.2em solid var(--ion-color-primary); 20 | } 21 | 22 | .reboot-mark { 23 | border-left: 0.2em solid var(--ion-color-disclaimer); 24 | } -------------------------------------------------------------------------------- /src/app/settings/settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/settings/settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/app/settings/settings.component.scss -------------------------------------------------------------------------------- /src/app/spn-view/download-progress/download-progress.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | GeoIP Update 5 | Around 200MB of assets 6 | 7 | 8 | 9 | The downloaded GeoIP data is used by Portmaster to see which countries your connections are going to, and to calculate the best routes for SPN. 10 |
11 |
12 |
Do you want to download it now?
13 |
Download will start as soon as WiFi is available
14 |
15 | 16 | Download Now 17 | Wait for Wifi 18 |
19 |
20 | 21 | Downloading GeoIP Data 22 | 23 | 24 | 25 | Downloaded {{Registry.Details.FinishedUpTo}}/{{Registry.Details.Resources.length}} 26 |
27 | {{Registry.Details.Resources[Registry.Details.FinishedUpTo]}} 28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | New Portmaster update is available 36 | 37 | 38 | Check F-Droid for the new version or go to our website for more details. 39 | 40 | -------------------------------------------------------------------------------- /src/app/spn-view/download-progress/download-progress.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/app/spn-view/download-progress/download-progress.component.scss -------------------------------------------------------------------------------- /src/app/spn-view/download-progress/download-progress.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectorRef, Component, OnDestroy, OnInit, Output, EventEmitter } from '@angular/core'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import GoBridge from 'src/app/plugins/go.bridge'; 5 | import { UpdaterService } from 'src/app/services/updater.service'; 6 | import { RegistryState, State } from 'src/app/services/updater.types'; 7 | 8 | 9 | @Component({ 10 | selector: 'app-download-progress', 11 | templateUrl: './download-progress.component.html', 12 | styleUrls: ['./download-progress.component.scss'], 13 | standalone: true, 14 | imports: [CommonModule, IonicModule] 15 | }) 16 | export class DownloadProgressComponent implements OnInit, OnDestroy { 17 | readonly State = State; 18 | 19 | Registry: RegistryState; 20 | HasUpdates: boolean = false; 21 | Timer: any = null; 22 | IsOnWifi: boolean = false; 23 | NewApk: boolean = false; 24 | 25 | WifiDownloadClicked: boolean = false; 26 | 27 | @Output() OnDownloadComplete = new EventEmitter(); 28 | 29 | constructor(private changeDetector: ChangeDetectorRef, private updaterService: UpdaterService) { } 30 | 31 | ngOnInit() { 32 | this.updaterService.watchState().subscribe((registry: RegistryState) => { 33 | console.log("Update state:", JSON.stringify(registry)); 34 | 35 | if(this.Registry?.ID == State.Downloading && registry.ID === State.Ready) { 36 | this.OnDownloadComplete.emit(); 37 | clearInterval(this.Timer); 38 | this.Timer = null; 39 | } 40 | 41 | this.HasUpdates = registry.Updates.PendingDownload?.length > 0; 42 | this.Registry = registry; 43 | GoBridge.IsOnWifiNetwork().then((onWifi: boolean) => { 44 | this.IsOnWifi = onWifi; 45 | }); 46 | 47 | if(this.HasUpdates) { 48 | this.wifiUpdater(); 49 | } 50 | 51 | GoBridge.NewApkAvaliable().then((newApk: boolean) => { 52 | this.NewApk = newApk; 53 | }); 54 | 55 | this.changeDetector.detectChanges(); 56 | }) 57 | } 58 | 59 | public wifiUpdater() { 60 | if(this.Timer !== null) { 61 | return; 62 | } 63 | this.Timer = setInterval(() => { 64 | GoBridge.IsOnWifiNetwork().then((onWifi: boolean) => { 65 | this.IsOnWifi = onWifi; 66 | this.changeDetector.detectChanges(); 67 | }); 68 | }, 10000) 69 | } 70 | 71 | ngOnDestroy(): void { 72 | clearInterval(this.Timer); 73 | } 74 | 75 | downloadNow(): void { 76 | GoBridge.DownloadPendingUpdates(); 77 | } 78 | 79 | downloadOnWifi(): void { 80 | this.WifiDownloadClicked = true; 81 | GoBridge.DownloadUpdatesOnWifiConnected(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/spn-view/notifications/notifications.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 |
8 | {{notification.Title}} 9 |
10 | {{notification.Message}}
11 |
12 |
13 |

No new notifications

14 |
15 | -------------------------------------------------------------------------------- /src/app/spn-view/notifications/notifications.component.scss: -------------------------------------------------------------------------------- 1 | @mixin trim($numLines: null) { 2 | @if $numLines !=null { 3 | display: -webkit-box; 4 | -webkit-line-clamp: $numLines; 5 | -webkit-box-orient: vertical; 6 | overflow: hidden; 7 | } 8 | 9 | @else { 10 | text-overflow: ellipsis; 11 | white-space: nowrap; 12 | overflow: hidden; 13 | display: block; 14 | } 15 | } 16 | 17 | .notification-list { 18 | width: 100%; 19 | background-color: #181818; 20 | // border: solid; 21 | border-top: 1px solid rgb(100 100 100); 22 | border-bottom: 1px solid rgb(100 100 100); 23 | padding: 0 0 0 0; 24 | margin: 0 0 0 0; 25 | } 26 | 27 | .notification-placeholder { 28 | text-align: center; 29 | vertical-align: middle; 30 | color: #8c8c8c; 31 | margin: 0.2em 0 0.2em 0; 32 | } 33 | 34 | .notification { 35 | width: 100%; 36 | --background: #252525; 37 | margin: 0.1em 0.1em 0.1em 0; 38 | padding: 0 0 0 0; 39 | } 40 | 41 | .notification-content { 42 | @include trim(1); 43 | overflow: ellipsis; 44 | color: #8c8c8c; 45 | } 46 | 47 | .notification-text { 48 | @include trim(2); 49 | overflow: ellipsis; 50 | color: #8c8c8c; 51 | } 52 | 53 | .bell-icon { 54 | margin-inline-end: 0.5em; 55 | margin-inline-start: 0; 56 | display: inline-block; 57 | vertical-align: middle; 58 | } 59 | 60 | .info { 61 | border-left: 0.4em solid #727272; 62 | } 63 | 64 | .warning { 65 | border-left: 0.4em solid var(--ion-color-disclaimer); 66 | } 67 | 68 | .prompt { 69 | border-left: 0.4em solid var(--ion-color-secondary); 70 | } 71 | 72 | .error { 73 | border-left: 0.4em solid var(--ion-color-danger); 74 | } -------------------------------------------------------------------------------- /src/app/spn-view/notifications/notifications.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; 2 | 3 | import { CommonModule } from "@angular/common"; 4 | import { AlertButton, AlertController, IonicModule } from "@ionic/angular"; 5 | import { Action, Notification, NotificationType, getNotificationTypeString } from "src/app/services/notifications.types"; 6 | import { NotificationsService } from "src/app/services/notifications.service"; 7 | 8 | @Component({ 9 | selector: "notifications", 10 | templateUrl: './notifications.component.html', 11 | styleUrls: ['./notifications.component.scss'], 12 | standalone: true, 13 | imports: [CommonModule, IonicModule] 14 | }) 15 | export class NotificationComponent implements OnInit { 16 | readonly types = NotificationType; 17 | 18 | notifications: Notification[]; 19 | 20 | constructor( 21 | private notificationService: NotificationsService, 22 | private alertController: AlertController, 23 | private changeDetector: ChangeDetectorRef) { } 24 | 25 | ngOnInit(): void { 26 | this.notificationService.new$ 27 | .subscribe((notifications: Notification[]): void => { 28 | this.notifications = notifications; 29 | this.changeDetector.detectChanges(); 30 | }); 31 | } 32 | 33 | open(notification: Notification): void { 34 | if(!notification.AvailableActions) { 35 | return; 36 | } 37 | 38 | let buttons: AlertButton[] = []; 39 | 40 | notification.AvailableActions.forEach((action: Action): void => { 41 | buttons.push({ 42 | text: action.Text, 43 | handler: (): void => { 44 | this.performAction(notification, action); 45 | } 46 | }); 47 | }); 48 | 49 | this.alertController.create({ 50 | header: notification.Title, 51 | subHeader: getNotificationTypeString(notification.Type), 52 | message: notification.Message, 53 | buttons: buttons, 54 | }).then((alert) => { 55 | alert.present(); 56 | }); 57 | } 58 | 59 | performAction(notification: Notification, action: Action): void { 60 | this.notificationService.execute(notification, action).subscribe(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/spn-view/security-lock/security-lock.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /src/app/spn-view/security-lock/security-lock.component.scss: -------------------------------------------------------------------------------- 1 | svg.shield { 2 | width: 100%; 3 | max-width: 13rem; 4 | 5 | transform: scale(0.95); 6 | 7 | path { 8 | top: 0px; 9 | left: 0px; 10 | transform-origin: center center; 11 | } 12 | 13 | .shield-one { 14 | transform: scale(.62); 15 | } 16 | 17 | .shield-two { 18 | animation-delay: -1.2s; 19 | opacity: .6; 20 | transform: scale(.8); 21 | } 22 | 23 | .shield-three { 24 | animation-delay: -2.5s; 25 | opacity: .4; 26 | transform: scale(1); 27 | } 28 | 29 | &.text-green-300 { 30 | filter: saturate(1.4); 31 | 32 | .shield-one { 33 | fill: var(--protection-ok-primary); 34 | } 35 | 36 | 37 | .shield-two { 38 | fill: var(--protection-ok-secondary); 39 | } 40 | 41 | .shield-three { 42 | fill: var(--protection-ok-tertiary); 43 | } 44 | 45 | .shield-warn, 46 | .shield-fail { 47 | display: none; 48 | } 49 | 50 | .shield-ok { 51 | stroke: var(--background); 52 | fill: none; 53 | transform: scale(.5); 54 | } 55 | } 56 | 57 | &.text-yellow-300 { 58 | filter: saturate(1.3); 59 | 60 | .shield-one { 61 | fill: var(--protection-warn-primary); 62 | } 63 | 64 | .shield-three, 65 | .shield-two { 66 | //animation: shield-pulse 3s linear; 67 | } 68 | 69 | .shield-two { 70 | fill: var(--protection-warn-secondary); 71 | } 72 | 73 | .shield-three { 74 | fill: var(--protection-warn-tertiary); 75 | } 76 | 77 | .shield-ok, 78 | .shield-fail { 79 | display: none; 80 | } 81 | 82 | .shield-warn { 83 | stroke: var(--background); 84 | fill: none; 85 | transform: scale(.5); 86 | } 87 | } 88 | 89 | &.text-red-300 { 90 | filter: saturate(1.3); 91 | 92 | .shield-one { 93 | fill: var(--protection-fail-primary); 94 | } 95 | 96 | .shield-three, 97 | .shield-two { 98 | //animation: shield-pulse 3s linear reverse; 99 | } 100 | 101 | .shield-two { 102 | fill: var(--protection-fail-secondary); 103 | } 104 | 105 | .shield-three { 106 | fill: var(--protection-fail-tertiary); 107 | } 108 | 109 | .shield-warn, 110 | .shield-ok { 111 | display: none; 112 | } 113 | 114 | .shield-fail { 115 | stroke: var(--background); 116 | fill: none; 117 | transform: scale(.45); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/spn-view/spn-button/spn-button.component.html: -------------------------------------------------------------------------------- 1 | Upgrade 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/spn-view/spn-button/spn-button.component.scss: -------------------------------------------------------------------------------- 1 | $width: 4em; 2 | $height: 2em; 3 | $border: 0.05em; 4 | 5 | .toggle-container { 6 | display: inline-block; 7 | width: $width; 8 | height: $height; 9 | border: $border solid var(--ion-color-pm-accent); 10 | border-radius: $height; 11 | position: relative; 12 | cursor: pointer; 13 | background-color: rgb(136 136 136); 14 | transition: border-color 300ms; 15 | 16 | .real-checkbox { 17 | position: absolute; 18 | clip: rect(0, 0, 0, 0); 19 | 20 | &+.toggle-button { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | right: 0; 25 | bottom: 0; 26 | border-radius: 30px; 27 | transition: all 100ms; 28 | margin: 0 0 0 0; 29 | content: ''; 30 | cursor: pointer; 31 | display: inline-block; 32 | width: $height - $border * 2; 33 | height: $height - $border * 2; 34 | background-color: white; 35 | border-radius: 50%; 36 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); 37 | transition: all 100ms ease-in-out; 38 | 39 | } 40 | 41 | &:checked+.toggle-button { 42 | background-color: var(--protection-ok-primary); 43 | 44 | &::before { 45 | margin-left: $height; 46 | } 47 | } 48 | } 49 | } 50 | 51 | .toggle-container.connected { 52 | background-color: var(--protection-ok-primary); 53 | 54 | .real-checkbox { 55 | &+.toggle-button { 56 | margin-left: $height; 57 | } 58 | } 59 | } 60 | 61 | .toggle-container.connecting { 62 | background-color: var(--ion-color-primary); 63 | 64 | .real-checkbox { 65 | &+.toggle-button { 66 | margin-left: $height; 67 | } 68 | } 69 | } 70 | 71 | .toggle-container.failed { 72 | background-color: var(--protection-fail-primary); 73 | 74 | .real-checkbox { 75 | &+.toggle-button { 76 | margin-left: $height; 77 | } 78 | } 79 | } 80 | 81 | .toggle-container.disabled { 82 | background-color: rgb(136 136 136); 83 | 84 | .real-checkbox { 85 | &+.toggle-button { 86 | background-color: rgb(136 136 136); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/app/spn-view/spn-button/spn-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from "@angular/core"; 2 | 3 | import { CommonModule } from "@angular/common"; 4 | import { IonicModule } from "@ionic/angular"; 5 | import { SPNStatus, UserProfile } from "src/app/lib/spn.types"; 6 | 7 | @Component({ 8 | selector: "spn-button", 9 | templateUrl: './spn-button.component.html', 10 | styleUrls: ['./spn-button.component.scss'], 11 | standalone: true, 12 | imports: [CommonModule, IonicModule] 13 | }) 14 | export class SPNButton { 15 | @Input() User: UserProfile; 16 | @Input() SPNStatus: SPNStatus; 17 | @Input() IsGeoIPDataAvailable: boolean; 18 | 19 | @Output() onStateChange = new EventEmitter(); 20 | @Output() onLogin = new EventEmitter(); 21 | 22 | constructor() { } 23 | 24 | async onClick() { 25 | if(!this.User?.username) { 26 | this.onLogin.emit(); 27 | return; 28 | } 29 | 30 | if(!this.IsGeoIPDataAvailable) { 31 | return; 32 | } 33 | 34 | switch(this.SPNStatus.Status) { 35 | case "disabled": { 36 | this.onStateChange.emit(true) 37 | this.SPNStatus.Status = "connecting"; 38 | break; 39 | } 40 | default: { 41 | this.onStateChange.emit(false); 42 | break; 43 | } 44 | } 45 | } 46 | 47 | GetButtonText(): string { 48 | if(!this.User?.username) { 49 | return "Login"; 50 | } 51 | 52 | if(!this.IsGeoIPDataAvailable) { 53 | return "Missing data" 54 | } 55 | 56 | if(this.SPNStatus == null) { 57 | return ""; 58 | } 59 | 60 | switch(this.SPNStatus.Status) { 61 | case "connected": { 62 | return "disable"; 63 | } 64 | case "disabled": { 65 | return "connect"; 66 | } 67 | case "connecting": { 68 | return "connecting"; 69 | } 70 | case "failed": { 71 | return "disable"; 72 | } 73 | } 74 | } 75 | 76 | GetButtonColor(): string { 77 | if(this.SPNStatus == null) { 78 | return ""; 79 | } 80 | 81 | if(this.SPNStatus.Status == "disabled") { 82 | return "primary"; 83 | } 84 | 85 | return "danger"; 86 | } 87 | } -------------------------------------------------------------------------------- /src/app/spn-view/spn-view.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

This version is not optimized for battery performance. We are working on improving this for future versions. 5 |

6 |
7 |
8 | 9 |
10 | 11 | 12 |

Shutdown:

13 |
14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 |

Account info:

22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | SPN 32 |

33 | 34 | Increase privacy protection 35 | 36 | 37 | Failed to connect 38 | 39 | 40 | Connecting to the SPN ... 41 | 42 | 43 | You're protected 44 | 45 |

46 |

47 | Missing GeoIP data 48 |

49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/app/spn-view/spn-view.component.scss: -------------------------------------------------------------------------------- 1 | .md body { 2 | --ion-item-background: #121212; 3 | } 4 | 5 | #container { 6 | text-align: center; 7 | 8 | position: absolute; 9 | left: 0; 10 | right: 0; 11 | top: 50%; 12 | transform: translateY(-50%); 13 | } 14 | 15 | #container strong { 16 | font-size: 20px; 17 | line-height: 26px; 18 | } 19 | 20 | #container p { 21 | font-size: 16px; 22 | line-height: 22px; 23 | 24 | color: #8c8c8c; 25 | 26 | margin: 0; 27 | } 28 | 29 | #container a { 30 | text-decoration: none; 31 | } 32 | 33 | .header { 34 | // position: relative; 35 | height: 25%; 36 | } 37 | 38 | .shield { 39 | position: relative; 40 | float: left; 41 | height: 10%; 42 | } 43 | 44 | .shutdown-button { 45 | // position: absolute; 46 | left: 85%; 47 | top: 10% 48 | } 49 | 50 | .user-button { 51 | // position: absolute; 52 | left: 85%; 53 | top: 35% 54 | } -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.scss: -------------------------------------------------------------------------------- 1 | ion-tab-bar { 2 | width: 100%; 3 | height: 6%; 4 | } 5 | 6 | ion-tab-button { 7 | --color: var(--ion-color-medium); 8 | --color-selected: var(--ion-color-primary); 9 | --opacity-selected:0.5; 10 | stroke: #ffffff; 11 | 12 | ion-icon { 13 | opacity: 0.5; 14 | } 15 | } 16 | 17 | ion-tab-button.tab-selected { 18 | stroke: #fff; 19 | ion-icon { 20 | opacity: 1; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, EnvironmentInjector, inject } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | @Component({ 5 | selector: 'app-tabs', 6 | templateUrl: 'tabs.page.html', 7 | styleUrls: ['tabs.page.scss'], 8 | standalone: true, 9 | imports: [IonicModule], 10 | }) 11 | export class TabsPage { 12 | public environmentInjector = inject(EnvironmentInjector); 13 | 14 | constructor() {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { TabsPage } from './tabs.page'; 3 | 4 | export const routes: Routes = [ 5 | { 6 | path: '', 7 | component: TabsPage, 8 | children: [ 9 | { 10 | path: 'main', 11 | loadComponent: () => 12 | import('../spn-view/spn-view.component').then((m) => m.SPNViewComponent), 13 | }, 14 | { 15 | path: 'help', 16 | loadComponent: () => import('../help/help.component').then((m) => m.HelpComponent), 17 | }, 18 | { 19 | path: 'settings', 20 | loadComponent: () => import('../settings/settings.component').then((m) => m.SettingsComponent), 21 | }, 22 | { 23 | path: '', 24 | redirectTo: '/main', 25 | pathMatch: 'full', 26 | }, 27 | ], 28 | }, 29 | { 30 | path: '', 31 | redirectTo: '/main', 32 | pathMatch: 'full', 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /src/app/types/issue.types.ts: -------------------------------------------------------------------------------- 1 | export class Section { 2 | title: string 3 | body: string 4 | } 5 | 6 | export class IssueRequest { 7 | title: string 8 | sections: Array
9 | } 10 | 11 | export class TicketRequest { 12 | title: string 13 | sections: Array
14 | repoName: string 15 | email: string 16 | } -------------------------------------------------------------------------------- /src/app/welcome/welcome.component.scss: -------------------------------------------------------------------------------- 1 | /* Without setting height the slides will take up the height of the slide's content */ 2 | ion-slides { 3 | height: 100%; 4 | } 5 | 6 | .logo { 7 | fill: #fff; 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/access-regional-content-easily.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/access-regional-content-easily.png -------------------------------------------------------------------------------- /src/assets/always-on-setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/always-on-setting.jpg -------------------------------------------------------------------------------- /src/assets/block-connections-setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/block-connections-setting.jpg -------------------------------------------------------------------------------- /src/assets/bye-bye-vpns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/bye-bye-vpns.png -------------------------------------------------------------------------------- /src/assets/easily-control-your-privacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/easily-control-your-privacy.png -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /src/assets/main-vpn-settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/main-vpn-settings.jpg -------------------------------------------------------------------------------- /src/assets/menu/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/menu/bell.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/assets/menu/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/menu/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/menu/shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 13 | 16 | 18 | -------------------------------------------------------------------------------- /src/assets/menu/spn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/menu/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/multiple-identities-for-each-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/portmaster-android/3226ceac3bec97887c4a44e9c7f49b9ec2805fb6/src/assets/multiple-identities-for-each-app.png -------------------------------------------------------------------------------- /src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | supportHub: "https://support.safing.io" 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * App Global CSS 3 | * ---------------------------------------------------------------------------- 4 | * Put style rules here that you want to apply globally. These styles are for 5 | * the entire app and not just one component. Additionally, this file can be 6 | * used as an entry point to import other CSS/Sass files to be included in the 7 | * output CSS. 8 | * For more information on global stylesheets, visit the documentation: 9 | * https://ionicframework.com/docs/layout/global-stylesheets 10 | */ 11 | 12 | /* Core CSS required for Ionic components to work properly */ 13 | @import "~@ionic/angular/css/core.css"; 14 | 15 | /* Basic CSS for apps built with Ionic */ 16 | @import "~@ionic/angular/css/normalize.css"; 17 | @import "~@ionic/angular/css/structure.css"; 18 | @import "~@ionic/angular/css/typography.css"; 19 | @import '~@ionic/angular/css/display.css'; 20 | 21 | /* Optional CSS utils that can be commented out */ 22 | @import "~@ionic/angular/css/padding.css"; 23 | @import "~@ionic/angular/css/float-elements.css"; 24 | @import "~@ionic/angular/css/text-alignment.css"; 25 | @import "~@ionic/angular/css/text-transformation.css"; 26 | @import "~@ionic/angular/css/flex-utils.css"; 27 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import '@angular/compiler' 2 | import { Provider, enableProdMode, importProvidersFrom } from '@angular/core'; 3 | import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; 4 | import { RouteReuseStrategy, provideRouter } from '@angular/router'; 5 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 6 | 7 | import { routes } from './app/app.routes'; 8 | import { AppComponent } from './app/app.component'; 9 | import { environment } from './environments/environment'; 10 | import { HttpClient, HttpHandler } from '@angular/common/http'; 11 | import { HttpGoBackend } from './app/services/http.backend'; 12 | import { WebsocketGoService } from './app/services/websocket.service'; 13 | import { WebsocketService } from './app/lib/websocket.service'; 14 | import { PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT, PortapiService } from './app/lib/portapi.service'; 15 | import { SPNService } from './app/lib/spn.service'; 16 | import { ConfigService } from './app/lib/config.service'; 17 | import { NotificationsService } from './app/services/notifications.service'; 18 | import { StatusService } from './app/services/status.service'; 19 | import { MarkdownService, SECURITY_CONTEXT } from 'ngx-markdown'; 20 | import { ShutdownService } from './app/services/shutdown.service'; 21 | import { UpdaterService } from './app/services/updater.service'; 22 | 23 | if (environment.production) { 24 | enableProdMode(); 25 | } 26 | 27 | export function provideHttpGoClient(): Provider[] { 28 | const providers: Provider[] = [ 29 | HttpClient, 30 | {provide: HttpHandler, useClass: HttpGoBackend}, 31 | ]; 32 | 33 | return providers; 34 | } 35 | 36 | bootstrapApplication(AppComponent, { 37 | providers: [ 38 | BrowserModule, 39 | {provide: WebsocketService, useClass: WebsocketGoService }, 40 | PortapiService, 41 | SPNService, 42 | NotificationsService, 43 | ConfigService, 44 | StatusService, 45 | UpdaterService, 46 | MarkdownService, 47 | ShutdownService, 48 | provideHttpGoClient(), 49 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 50 | importProvidersFrom(IonicModule.forRoot({})), 51 | provideRouter(routes), 52 | {provide: PORTMASTER_HTTP_API_ENDPOINT, useValue: 'internal:'}, 53 | {provide: PORTMASTER_WS_API_ENDPOINT, useValue: 'not_used'}, 54 | {provide: SECURITY_CONTEXT, useValue: 0}, 55 | ], 56 | }); 57 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | import './zone-flags'; 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting() 14 | ); 15 | -------------------------------------------------------------------------------- /src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | // eslint-disable-next-line no-underscore-dangle 6 | (window as any).__Zone_disable_customElements = true; 7 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ], 19 | "useDefineForClassFields": false 20 | }, 21 | "angularCompilerOptions": { 22 | "enableI18nLegacyMessageIdFormat": false, 23 | "strictInjectionParameters": true, 24 | "strictInputAccessModifiers": true, 25 | "strictTemplates": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------