├── .watchmanconfig ├── app.json ├── src ├── providers │ ├── DigitalOcean │ │ ├── constants.tsx │ │ ├── do-api-callback-html.tsx │ │ ├── ClientFacade.tsx │ │ ├── Deploy.tsx │ │ ├── ApiClient.tsx │ │ └── cloudconfig.tsx │ ├── types │ │ ├── Account.tsx │ │ ├── Provider.tsx │ │ ├── Region.tsx │ │ ├── Token.tsx │ │ ├── VPNCredentials.tsx │ │ ├── Server.tsx │ │ └── Client.tsx │ ├── with_client.tsx │ ├── index.tsx │ └── client.tsx ├── store │ ├── types │ │ └── Notify.tsx │ ├── withInitState.tsx │ └── store.tsx ├── exceptions │ ├── ProviderUnexpectedError.tsx │ ├── ProviderAuthenticationError.tsx │ ├── ProviderInvalidContentError.tsx │ └── AbstractError.tsx ├── theme.tsx ├── screens │ ├── constants.tsx │ ├── ProviderRegisterScreen │ │ ├── index.tsx │ │ ├── styles.tsx │ │ ├── buttons.tsx │ │ └── digitalocean_login.tsx │ ├── MainScreen │ │ ├── layout.tsx │ │ ├── current_server.tsx │ │ ├── style.tsx │ │ ├── notifications.tsx │ │ ├── buttons.tsx │ │ ├── index.tsx │ │ └── linking_listener.tsx │ ├── SettingsScreen │ │ ├── styles.tsx │ │ ├── buttons.tsx │ │ ├── server_item.tsx │ │ ├── LogViewerScreen │ │ │ └── index.tsx │ │ ├── provider_list_item.tsx │ │ ├── ProviderRegionScreen │ │ │ ├── index.tsx │ │ │ └── region_list_item.tsx │ │ └── index.tsx │ ├── RegisterScreens.tsx │ ├── SSHTerminalScreen │ │ ├── terminal_server.tsx │ │ └── index.tsx │ └── useScreen.tsx ├── helper.tsx ├── app.tsx ├── logger │ └── index.tsx ├── ssh │ ├── keygen.tsx │ └── client.tsx ├── static_server.tsx └── keychain │ └── index.tsx ├── screenshots └── app.jpeg ├── .gitattributes ├── .eslintrc.js ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ ├── Fontisto.ttf │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── zudvpn │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ ├── build_defs.bzl │ ├── _BUCK │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── build.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── index.js ├── babel.config.js ├── ios ├── ZudVPN │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 29.png │ │ │ ├── 40.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 180.png │ │ │ └── Contents.json │ │ └── SplashIcon.imageset │ │ │ ├── 256.png │ │ │ ├── 512.png │ │ │ ├── 1024.png │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── ZudVPN.entitlements │ ├── AppDelegate.m │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── ZudVPN.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── ZudVPNTests │ ├── Info.plist │ └── ZudVPNTests.m ├── ZudVPN-tvOSTests │ └── Info.plist ├── Podfile ├── ZudVPN-tvOS │ └── Info.plist └── ZudVPN.xcodeproj │ └── xcshareddata │ └── xcschemes │ ├── ZudVPN.xcscheme │ └── ZudVPN-tvOS.xcscheme ├── jest.config.js ├── .buckconfig ├── .prettierrc.js ├── tests └── App-test.js ├── metro.config.js ├── tsconfig.json ├── .github └── FUNDING.yml ├── docs ├── INSTALL.md └── REINSTALL.md ├── .gitignore ├── assets └── terminal │ ├── index.html │ ├── xterm-addon-fit.js │ ├── xterm.css │ └── xterm-addon-fit.js.map ├── package.json ├── .flowconfig └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZudVPN", 3 | "displayName": "ZudVPN" 4 | } -------------------------------------------------------------------------------- /src/providers/DigitalOcean/constants.tsx: -------------------------------------------------------------------------------- 1 | export const SERVER_TAG = 'zudvpn'; 2 | -------------------------------------------------------------------------------- /screenshots/app.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/screenshots/app.jpeg -------------------------------------------------------------------------------- /src/providers/types/Account.tsx: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | email: string; 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # specific for windows script files 2 | *.bat text eol=crlf 3 | *.pbxproj -text 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import app from './src/app'; 2 | 3 | console.disableYellowBox = true; 4 | 5 | app(); 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/providers/types/Provider.tsx: -------------------------------------------------------------------------------- 1 | export interface Provider { 2 | id: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/store/types/Notify.tsx: -------------------------------------------------------------------------------- 1 | export interface Notify { 2 | (type: string, notification: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ZudVPN 3 | 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /src/providers/types/Region.tsx: -------------------------------------------------------------------------------- 1 | export interface Region { 2 | name: string; 3 | slug: string; 4 | available: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 4 | }; -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/SplashIcon.imageset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/SplashIcon.imageset/256.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/SplashIcon.imageset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/SplashIcon.imageset/512.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/SplashIcon.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/ios/ZudVPN/Images.xcassets/SplashIcon.imageset/1024.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zudvpn/ZudVPN/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/exceptions/ProviderUnexpectedError.tsx: -------------------------------------------------------------------------------- 1 | import AbstractError from './AbstractError'; 2 | 3 | export default class ProviderUnexpectedError extends AbstractError {} 4 | -------------------------------------------------------------------------------- /src/exceptions/ProviderAuthenticationError.tsx: -------------------------------------------------------------------------------- 1 | import AbstractError from './AbstractError'; 2 | 3 | export default class ProviderAuthenticationError extends AbstractError {} 4 | -------------------------------------------------------------------------------- /src/exceptions/ProviderInvalidContentError.tsx: -------------------------------------------------------------------------------- 1 | import AbstractError from './AbstractError'; 2 | 3 | export default class ProviderInvalidContentError extends AbstractError {} 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | jsxBracketSameLine: true, 4 | printWidth: 120, 5 | singleQuote: true, 6 | tabWidth: 4, 7 | trailingComma: 'all', 8 | }; 9 | -------------------------------------------------------------------------------- /src/providers/types/Token.tsx: -------------------------------------------------------------------------------- 1 | import { Account } from 'providers/types/Account'; 2 | 3 | export interface Token { 4 | provider: string; 5 | accessToken: string; 6 | account?: Account | null; 7 | } 8 | -------------------------------------------------------------------------------- /src/exceptions/AbstractError.tsx: -------------------------------------------------------------------------------- 1 | export default class AbstractError extends Error { 2 | constructor(message?: string) { 3 | super(message); 4 | this.name = this.constructor.name; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/theme.tsx: -------------------------------------------------------------------------------- 1 | export const BACKGROUND_PRIMARY = '#000'; 2 | export const BACKGROUND_SECONDARY = '#212121'; 3 | export const COLOR_PRIMARY = '#ff8800'; 4 | export const COLOR_SECONDARY = '#c4c4c4'; 5 | export const COLOR_TERTIARY = '#0069ff'; 6 | -------------------------------------------------------------------------------- /ios/ZudVPN/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /ios/ZudVPN/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/providers/types/VPNCredentials.tsx: -------------------------------------------------------------------------------- 1 | import { Server } from 'providers/types/Server'; 2 | 3 | export interface VPNCredentials { 4 | server: Server; 5 | ipAddress: string; 6 | domain: string; 7 | username: string; 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/providers/types/Server.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'providers/types/Provider'; 2 | import { Region } from 'providers/types/Region'; 3 | 4 | export interface Server { 5 | uid: string; 6 | provider: Provider; 7 | name: string; 8 | region: Region; 9 | ipv4Address: string; 10 | } 11 | -------------------------------------------------------------------------------- /ios/ZudVPN.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/ZudVPN.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ZudVPN' 2 | include ':react-native-vector-icons' 3 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') 4 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 5 | include ':app' 6 | -------------------------------------------------------------------------------- /src/providers/with_client.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useStore } from '../store/store'; 3 | import Client from './client'; 4 | 5 | const withClient = (Component: any) => (props: any) => { 6 | const [{ providerTokens }] = useStore(); 7 | 8 | const client = new Client(providerTokens); 9 | 10 | return ; 11 | }; 12 | 13 | export default withClient; 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/screens/constants.tsx: -------------------------------------------------------------------------------- 1 | export const MAIN_SCREEN = 'navigation.main.screen'; 2 | export const PROVIDER_REGISTER_SCREEN = 'navigation.provider_register.screen'; 3 | export const LOG_FILE_VIEWER_SCREEN = 'navigation.log_file_viewer.screen'; 4 | export const SETTINGS_SCREEN = 'navigation.settings.screen'; 5 | export const PROVIDER_REGION_SCREEN = 'navigation.provider_region.screen'; 6 | export const SSH_TERMINAL_SCREEN = 'navigation.ssh_terminal.screen'; 7 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/zudvpn/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zudvpn; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "ZudVPN"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/providers/DigitalOcean/do-api-callback-html.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return `

Loading...

3 | `; 7 | }; 8 | -------------------------------------------------------------------------------- /ios/ZudVPN/ZudVPN.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.vpn.api 6 | 7 | allow-vpn 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)com.zudvpn.app 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/screens/ProviderRegisterScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DigitalOceanLogin from './digitalocean_login'; 3 | import { Provider } from 'providers/types/Provider'; 4 | 5 | interface Props { 6 | provider: Provider; 7 | } 8 | 9 | const ProviderRegisterScreen = (props: Props) => { 10 | if (props.provider.id === 'digitalocean') { 11 | return ; 12 | } 13 | 14 | return <>; 15 | }; 16 | 17 | export default ProviderRegisterScreen; 18 | -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/SplashIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "256.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "512.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "1024.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /src/providers/types/Client.tsx: -------------------------------------------------------------------------------- 1 | import { Region } from 'providers/types/Region'; 2 | import { Notify } from 'store/types/Notify'; 3 | import { VPNCredentials } from 'providers/types/VPNCredentials'; 4 | import { Server } from 'providers/types/Server'; 5 | 6 | export interface Client { 7 | token: string; 8 | createServer(region: Region, notify: Notify): Promise; 9 | readServerVPN(server: Server, notify: Notify): Promise; 10 | getServers(): Promise; 11 | getRegions(): Promise; 12 | deleteServer(server: Server): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/screens/MainScreen/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import styles from './style'; 3 | import LinkingListener from './linking_listener'; 4 | import { Text, View } from 'react-native'; 5 | 6 | const Layout = (props: PropsWithChildren) => { 7 | return ( 8 | 9 | 10 | 11 | ZudVPN 12 | {props.children} 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Layout; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "react", 8 | "lib": ["esnext"], 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "strict": true, 12 | "target": "esnext", 13 | "baseUrl": ".", 14 | "paths": { 15 | "*": ["src/*"], 16 | "tests": ["tests/*"] 17 | }, 18 | "skipLibCheck": true, 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "babel.config.js", 23 | "metro.config.js", 24 | "jest.config.js" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/screens/ProviderRegisterScreen/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | button: { 5 | borderColor: '#0069ff', 6 | borderWidth: 1, 7 | borderRadius: 3, 8 | width: '100%', 9 | alignItems: 'center', 10 | padding: 20, 11 | }, 12 | button_label: { 13 | color: '#0069ff', 14 | fontWeight: '500', 15 | fontSize: 22, 16 | }, 17 | button_sublabel: { 18 | padding: 5, 19 | alignSelf: 'flex-end', 20 | position: 'absolute', 21 | fontSize: 11, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/screens/MainScreen/current_server.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton } from './buttons'; 3 | import { useStore } from '../../store/store'; 4 | import useScreen from '../useScreen'; 5 | 6 | const CurrentServer = () => { 7 | const [{ currentServer }] = useStore(); 8 | const { SettingsScreenModal } = useScreen(); 9 | 10 | if (currentServer) { 11 | const label = `Provider: ${currentServer.provider.name}\nRegion: ${currentServer.region.name}\nIP Address: ${currentServer.ipv4Address}`; 12 | return ; 13 | } 14 | 15 | return null; 16 | }; 17 | 18 | export default CurrentServer; 19 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: miniyarov 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/screens/ProviderRegisterScreen/buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TouchableOpacity } from 'react-native'; 3 | import styles from './styles'; 4 | 5 | interface Props { 6 | label: string; 7 | labelStyle: object; 8 | buttonStyle: object; 9 | onPress: any; 10 | sublabel?: string; 11 | } 12 | 13 | export const ProviderButton = ({ label, labelStyle = {}, buttonStyle = {}, onPress, sublabel = '' }: Props) => ( 14 | 15 | {label} 16 | {sublabel !== '' && {sublabel}} 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /src/helper.tsx: -------------------------------------------------------------------------------- 1 | import { Token } from 'providers/types/Token'; 2 | 3 | export function sleep(ms: number) { 4 | return new Promise((resolve) => setTimeout(resolve, ms)); 5 | } 6 | 7 | export function parse_linking_url_token(url: string): Token | null { 8 | let params = url 9 | .split('?')[1] 10 | .split('&') 11 | .reduce(function (result: any, item: string) { 12 | let parts = item.split('='); 13 | result[parts[0]] = parts[1]; 14 | return result; 15 | }, {}); 16 | 17 | if (Object.keys(params).length > 0 && params.hasOwnProperty('access_token')) { 18 | return { 19 | accessToken: params.access_token, 20 | provider: params.provider, 21 | account: null, 22 | }; 23 | } 24 | 25 | return null; 26 | } 27 | -------------------------------------------------------------------------------- /ios/ZudVPNTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/ZudVPN-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | export const AVAILABLE_PROVIDERS = [ 2 | { 3 | id: 'digitalocean', 4 | name: 'DigitalOcean', 5 | available: true, 6 | }, 7 | { 8 | id: 'aws', 9 | name: 'Amazon Web Services', 10 | available: false, 11 | }, 12 | { 13 | id: 'gcp', 14 | name: 'Google Cloud Platform', 15 | available: false, 16 | }, 17 | { 18 | id: 'azure', 19 | name: 'Microsoft Azure', 20 | available: false, 21 | }, 22 | { 23 | id: 'alibaba', 24 | name: 'Alibaba Cloud', 25 | available: false, 26 | }, 27 | { 28 | id: 'ovh', 29 | name: 'OVH Cloud', 30 | available: false, 31 | }, 32 | { 33 | id: 'vultr', 34 | name: 'Vultr', 35 | available: false, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /src/screens/SettingsScreen/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { COLOR_SECONDARY, COLOR_TERTIARY } from '../../theme'; 3 | 4 | export default StyleSheet.create({ 5 | server_container: { 6 | borderColor: COLOR_TERTIARY, 7 | borderWidth: 1, 8 | borderRadius: 3, 9 | margin: 20, 10 | }, 11 | button_container: { 12 | flexDirection: 'row', 13 | justifyContent: 'space-between', 14 | borderTopWidth: 1, 15 | borderTopColor: COLOR_TERTIARY, 16 | alignItems: 'center', 17 | }, 18 | button_separator: { 19 | height: '100%', 20 | borderWidth: 0.5, 21 | borderColor: COLOR_TERTIARY, 22 | }, 23 | section_title: { 24 | color: COLOR_SECONDARY, 25 | fontSize: 12, 26 | padding: 15, 27 | paddingBottom: 2, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | ZudVPN uses iOS network API to create a VPN profile. Apple mandates apps to use `Personal VPN` entitlement to interact with VPN profiles. 2 | However, `Personal VPN` entitlement is restricted to registered Apple Developers. As such, you must have a developer account to run ZudVPN on an iOS Device during development. 3 | 4 | Prerequisites (Also refer to [React Native Setup](https://reactnative.dev/docs/environment-setup) page) 5 | - Xcode 6 | - Xcode command line tools 7 | - Node 8 | - Yarn 9 | 10 | Install 11 | - `yarn install` 12 | - `npx pod-install` 13 | - `npx react-native run-ios` 14 | 15 | Limitation on iOS Simulator: Apple does not allow VPN Profile installation on iOS Simulator. Thus, you will be able to create VPN Server and interact with it using in-app terminal, however, you cannot install VPN Profile on iOS Simulator. You must use an iOS Device to fully run this application. 16 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { Navigation } from 'react-native-navigation'; 2 | import RegisterScreens from './screens/RegisterScreens'; 3 | import { MAIN_SCREEN } from './screens/constants'; 4 | import { BACKGROUND_SECONDARY, COLOR_SECONDARY } from './theme'; 5 | 6 | export default function app() { 7 | RegisterScreens(); 8 | 9 | Navigation.events().registerAppLaunchedListener(() => { 10 | Navigation.setDefaultOptions({ 11 | topBar: { 12 | title: { 13 | color: COLOR_SECONDARY, 14 | }, 15 | background: { 16 | color: BACKGROUND_SECONDARY, 17 | }, 18 | }, 19 | }); 20 | 21 | Navigation.setRoot({ 22 | root: { 23 | component: { 24 | name: MAIN_SCREEN, 25 | }, 26 | }, 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/screens/MainScreen/style.tsx: -------------------------------------------------------------------------------- 1 | import { Dimensions, StyleSheet } from 'react-native'; 2 | import { BACKGROUND_PRIMARY, BACKGROUND_SECONDARY, COLOR_PRIMARY } from '../../theme'; 3 | 4 | export default StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | width: '100%', 8 | alignItems: 'center', 9 | position: 'relative', 10 | paddingTop: '70%', 11 | backgroundColor: BACKGROUND_PRIMARY, 12 | }, 13 | curtain: { 14 | flex: 1, 15 | height: Dimensions.get('window').width, 16 | width: Dimensions.get('window').width * 2, 17 | position: 'absolute', 18 | backgroundColor: BACKGROUND_SECONDARY, 19 | borderBottomStartRadius: Dimensions.get('window').height, 20 | borderBottomEndRadius: Dimensions.get('window').height, 21 | }, 22 | logo: { 23 | color: COLOR_PRIMARY, 24 | position: 'absolute', 25 | top: 50, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '10.0' 5 | 6 | target 'ZudVPN' do 7 | # Pods for ZudVPN 8 | config = use_native_modules! 9 | use_react_native!(:path => config["reactNativePath"]) 10 | 11 | pod 'NMSSH', '2.2.8' 12 | pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' 13 | 14 | target 'ZudVPNTests' do 15 | inherit! :complete 16 | # Pods for testing 17 | end 18 | 19 | # Enables Flipper. 20 | # 21 | # Note that if you have use_frameworks! enabled, Flipper will not work and 22 | # you should disable these next few lines. 23 | #use_flipper! 24 | #post_install do |installer| 25 | # flipper_post_install(installer) 26 | #end 27 | end 28 | 29 | target 'ZudVPN-tvOS' do 30 | # Pods for ZudVPN-tvOS 31 | 32 | target 'ZudVPN-tvOSTests' do 33 | inherit! :search_paths 34 | # Pods for testing 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/logger/index.tsx: -------------------------------------------------------------------------------- 1 | import { logger, transportFunctionType } from 'react-native-logs'; 2 | import { rnFsFileAsync } from 'react-native-logs/dist/transports/rnFsFileAsync'; 3 | import { colorConsoleAfterInteractions } from 'react-native-logs/dist/transports/colorConsoleAfterInteractions'; 4 | 5 | export const APPLICATION_LOG_FILENAME = 'application_logs'; 6 | 7 | let transport: transportFunctionType = (msg, level, options) => { 8 | if (__DEV__) { 9 | colorConsoleAfterInteractions(msg, level, options); 10 | } 11 | rnFsFileAsync(msg, level, options); 12 | }; 13 | 14 | const config = { 15 | levels: { 16 | debug: 0, 17 | info: 1, 18 | progress: 2, 19 | success: 3, 20 | warn: 4, 21 | error: 5, 22 | }, 23 | transport, 24 | transportOptions: { 25 | dateFormat: 'utc', 26 | loggerName: APPLICATION_LOG_FILENAME, 27 | }, 28 | }; 29 | 30 | const log = logger.createLogger(config); 31 | 32 | if (!__DEV__) { 33 | log.setSeverity('info'); 34 | } 35 | 36 | export default log; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !android/app/debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /src/ssh/keygen.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { RSA } from 'react-native-rsa-native'; 4 | // @ts-ignore 5 | import forge from 'node-forge'; 6 | 7 | export interface Keypair { 8 | authorizedKey: string; 9 | fingerprint: string; 10 | publicKey: string; 11 | privateKey: string; 12 | } 13 | 14 | class Keygen { 15 | static async generateKeyPair(): Promise { 16 | let keys = await RSA.generateKeys(4096); 17 | 18 | let privateKey = keys.private; 19 | let publicKey = keys.public; 20 | 21 | let forgePrivateKey = forge.pki.privateKeyFromPem(privateKey); 22 | let forgePublicKey = forge.pki.setRsaPublicKey(forgePrivateKey.n, forgePrivateKey.e); 23 | 24 | let authorizedKey = forge.ssh.publicKeyToOpenSSH(forgePublicKey); 25 | let fingerprint = forge.ssh.getPublicKeyFingerprint(forgePublicKey, { encoding: 'hex', delimiter: ':' }); 26 | 27 | return { 28 | authorizedKey, 29 | fingerprint, 30 | publicKey, 31 | privateKey, 32 | }; 33 | } 34 | } 35 | 36 | export default Keygen; 37 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/screens/SettingsScreen/buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TouchableOpacity } from 'react-native'; 3 | import { COLOR_SECONDARY } from '../../theme'; 4 | 5 | interface Props { 6 | label: string; 7 | onPress: any; 8 | labelStyle?: object; 9 | } 10 | 11 | export const SegmentButton = ({ label, labelStyle, onPress }: Props) => ( 12 | 19 | {label} 20 | 21 | ); 22 | 23 | export const AddButton = ({ onPress }: { onPress: any }) => ( 24 | 35 | Add Server 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "29.0.2" 6 | minSdkVersion = 16 7 | compileSdkVersion = 29 8 | targetSdkVersion = 29 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.5.3") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://www.jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/screens/RegisterScreens.tsx: -------------------------------------------------------------------------------- 1 | import { Navigation } from 'react-native-navigation'; 2 | 3 | import MainScreen from './MainScreen'; 4 | import ProviderRegisterScreen from './ProviderRegisterScreen'; 5 | import LogFileViewerScreen from './SettingsScreen/LogViewerScreen'; 6 | import SettingsScreen from './SettingsScreen'; 7 | import ProviderRegionScreen from './SettingsScreen/ProviderRegionScreen'; 8 | import SSHTerminalScreen from './SSHTerminalScreen'; 9 | 10 | import { 11 | LOG_FILE_VIEWER_SCREEN, 12 | MAIN_SCREEN, 13 | PROVIDER_REGION_SCREEN, 14 | PROVIDER_REGISTER_SCREEN, 15 | SETTINGS_SCREEN, 16 | SSH_TERMINAL_SCREEN, 17 | } from './constants'; 18 | 19 | export default function RegisterScreens() { 20 | Navigation.registerComponent(MAIN_SCREEN, () => MainScreen); 21 | Navigation.registerComponent(PROVIDER_REGISTER_SCREEN, () => ProviderRegisterScreen); 22 | Navigation.registerComponent(LOG_FILE_VIEWER_SCREEN, () => LogFileViewerScreen); 23 | Navigation.registerComponent(SETTINGS_SCREEN, () => SettingsScreen); 24 | Navigation.registerComponent(PROVIDER_REGION_SCREEN, () => ProviderRegionScreen); 25 | Navigation.registerComponent(SSH_TERMINAL_SCREEN, () => SSHTerminalScreen); 26 | } 27 | -------------------------------------------------------------------------------- /src/screens/SSHTerminalScreen/terminal_server.tsx: -------------------------------------------------------------------------------- 1 | import NativeStaticServer from 'react-native-static-server'; 2 | import RNFS from 'react-native-fs'; 3 | 4 | class TerminalServer { 5 | private static instance: TerminalServer; 6 | private terminalServer: NativeStaticServer; 7 | 8 | private constructor(terminalServer: NativeStaticServer) { 9 | this.terminalServer = terminalServer; 10 | } 11 | 12 | public static getInstance(): TerminalServer { 13 | if (!TerminalServer.instance) { 14 | TerminalServer.instance = new TerminalServer( 15 | new NativeStaticServer(0, RNFS.MainBundlePath + '/assets/terminal', { localOnly: true }), 16 | ); 17 | } 18 | 19 | return TerminalServer.instance; 20 | } 21 | 22 | async serveTerminal(): Promise { 23 | let url = (await this.terminalServer.isRunning()) 24 | ? this.terminalServer._origin 25 | : await this.terminalServer.start(); 26 | 27 | console.log('URL SERVED', url); 28 | 29 | return url as string; 30 | } 31 | 32 | stop() { 33 | this.terminalServer.stop(); 34 | } 35 | } 36 | 37 | export default TerminalServer.getInstance(); 38 | -------------------------------------------------------------------------------- /src/static_server.tsx: -------------------------------------------------------------------------------- 1 | import NativeStaticServer from 'react-native-static-server'; 2 | import RNFS from 'react-native-fs'; 3 | 4 | class StaticServer { 5 | private static instance: StaticServer; 6 | private staticServer: NativeStaticServer; 7 | 8 | private constructor(staticServer: NativeStaticServer) { 9 | this.staticServer = staticServer; 10 | } 11 | 12 | public static getInstance(): StaticServer { 13 | if (!StaticServer.instance) { 14 | StaticServer.instance = new StaticServer( 15 | new NativeStaticServer(8080, RNFS.DocumentDirectoryPath + '/config', { localOnly: true }), 16 | ); 17 | } 18 | 19 | return StaticServer.instance; 20 | } 21 | 22 | async serveHtml(html: string) { 23 | await RNFS.mkdir(RNFS.DocumentDirectoryPath + '/config', { NSURLIsExcludedFromBackupKey: true }); 24 | const path = RNFS.DocumentDirectoryPath + '/config/callback.html'; 25 | 26 | await RNFS.writeFile(path, html, 'utf8'); 27 | 28 | let url = (await this.staticServer.isRunning()) ? this.staticServer._origin : await this.staticServer.start(); 29 | 30 | return url + '/callback.html'; 31 | } 32 | 33 | stop() { 34 | this.staticServer.stop(); 35 | } 36 | } 37 | 38 | export default StaticServer.getInstance(); 39 | -------------------------------------------------------------------------------- /src/keychain/index.tsx: -------------------------------------------------------------------------------- 1 | import * as RNKeychain from 'react-native-keychain'; 2 | import { Keypair } from 'ssh/keygen'; 3 | import { Token } from 'providers/types/Token'; 4 | import { Server } from 'providers/types/Server'; 5 | 6 | export const INITIAL_STATE_KEY = 'INITIAL_STATE_KEY'; 7 | 8 | interface State { 9 | privacyAccepted: boolean; 10 | providerTokens: Token[]; 11 | currentServer: Server | null; 12 | } 13 | 14 | const Keychain = { 15 | setInitialState: (state: State) => { 16 | Keychain.set(INITIAL_STATE_KEY, state); 17 | }, 18 | 19 | getInitialState: async () => { 20 | return await Keychain.get(INITIAL_STATE_KEY); 21 | }, 22 | 23 | setSSHKeyPair: (name: string, keypair: Keypair) => { 24 | Keychain.set(name, keypair); 25 | }, 26 | 27 | getSSHKeyPair: async (name: string) => { 28 | return await Keychain.get(name); 29 | }, 30 | 31 | set: (key: string, value: any) => { 32 | RNKeychain.setInternetCredentials(key, '', JSON.stringify(value)); 33 | }, 34 | 35 | get: async (key: string) => { 36 | const credentials = await RNKeychain.getInternetCredentials(key); 37 | 38 | if (credentials) { 39 | return JSON.parse(credentials.password); 40 | } 41 | 42 | return false; 43 | }, 44 | }; 45 | 46 | export default Keychain; 47 | -------------------------------------------------------------------------------- /src/store/withInitState.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, FC } from 'react'; 2 | import { ActivityIndicator, View } from 'react-native'; 3 | import { useStore } from './store'; 4 | import logger from '../logger'; 5 | import Keychain from '../keychain'; 6 | import { BACKGROUND_PRIMARY } from '../theme'; 7 | 8 | const withInitState = (Component: FC) => (props: any) => { 9 | const [loading, setLoading] = useState(true); 10 | const [, { initState }] = useStore(); 11 | 12 | useEffect(() => { 13 | const init = async () => { 14 | logger.debug('Initializing state'); 15 | 16 | const state = await Keychain.getInitialState(); 17 | 18 | if (!state) { 19 | logger.debug('Initial state is not present'); 20 | } else { 21 | logger.debug(['parsed state', state]); 22 | initState(state); 23 | } 24 | 25 | setLoading(false); 26 | }; 27 | 28 | init(); 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, []); 31 | 32 | return ( 33 | 34 | {loading ? : } 35 | 36 | ); 37 | }; 38 | 39 | export default withInitState; 40 | -------------------------------------------------------------------------------- /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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.54.0 29 | -------------------------------------------------------------------------------- /src/screens/MainScreen/notifications.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, Text, View } from 'react-native'; 3 | import { useStore } from '../../store/store'; 4 | import { COLOR_SECONDARY } from '../../theme'; 5 | 6 | const Notifications = () => { 7 | const [{ notifications }] = useStore(); 8 | 9 | if (notifications.length === 0) { 10 | return <>; 11 | } 12 | 13 | return ( 14 | 15 | {notifications.slice(0, 5).map((notification, i) => { 16 | return ( 17 | 18 | {i === 0 && notification.type === 'progress' && } 19 | 26 | {' '} 27 | {notification.notification} 28 | 29 | 30 | ); 31 | })} 32 | 33 | ); 34 | }; 35 | 36 | export default Notifications; 37 | -------------------------------------------------------------------------------- /assets/terminal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Terminal 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Terminal feature is experimental. Use at your own risk.

17 | 36 | 37 | -------------------------------------------------------------------------------- /android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.zudvpn", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.zudvpn", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /ios/ZudVPN/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} -------------------------------------------------------------------------------- /ios/ZudVPN-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/screens/SettingsScreen/server_item.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from 'react-native'; 2 | import styles from './styles'; 3 | import { SegmentButton } from './buttons'; 4 | import React from 'react'; 5 | import logger from '../../logger'; 6 | import useScreen from '../useScreen'; 7 | import { COLOR_SECONDARY } from '../../theme'; 8 | import { Server } from 'providers/types/Server'; 9 | 10 | interface Props { 11 | server: Server; 12 | select: any; 13 | destroy: any; 14 | } 15 | 16 | const ServerItem = ({ server, select, destroy }: Props) => { 17 | const { SSHTerminalScreenModal } = useScreen(); 18 | 19 | const sshTerminal = (name: string, ipv4Address: string) => () => { 20 | logger.debug(['SSH Terminal connection to: ', name]); 21 | SSHTerminalScreenModal(name, ipv4Address); 22 | }; 23 | 24 | return ( 25 | 26 | 27 | Provider: {server.provider.name} 28 | Region: {server.region.name} 29 | IP Address: {server.ipv4Address} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default ServerItem; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZudVPN", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "test": "jest", 10 | "lint": "eslint ." 11 | }, 12 | "dependencies": { 13 | "@react-native-community/async-storage": "^1.12.0", 14 | "node-forge": "^0.10.0", 15 | "react": "16.13.1", 16 | "react-native": "0.63.3", 17 | "react-native-elements": "^2.3.2", 18 | "react-native-fs": "^2.16.6", 19 | "react-native-get-random-values": "^1.5.0", 20 | "react-native-keychain": "^6.2.0", 21 | "react-native-logs": "^2.2.1", 22 | "react-native-navigation": "^3.7.0", 23 | "react-native-network-extension": "^0.1.32", 24 | "react-native-rsa-native": "^1.1.4", 25 | "react-native-safari-view": "^2.1.0", 26 | "react-native-ssh-sftp": "shaqian/react-native-ssh-sftp#master", 27 | "react-native-static-server": "futurepress/react-native-static-server#master", 28 | "react-native-vector-icons": "^7.1.0", 29 | "react-native-webview": "^11.0.2", 30 | "react-sweet-state": "^2.3.1", 31 | "uuid": "^8.3.0", 32 | "xterm": "^4.9.0", 33 | "xterm-addon-fit": "^0.4.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.8.4", 37 | "@babel/runtime": "^7.8.4", 38 | "@react-native-community/eslint-config": "^1.1.0", 39 | "@types/jest": "^26.0.18", 40 | "@types/react": "^17.0.0", 41 | "@types/react-native": "^0.63.37", 42 | "@types/react-test-renderer": "^17.0.0", 43 | "babel-jest": "^25.1.0", 44 | "eslint": "^6.5.1", 45 | "jest": "^25.1.0", 46 | "metro-react-native-babel-preset": "^0.59.0", 47 | "react-test-renderer": "16.13.1", 48 | "typescript": "^4.1.2" 49 | }, 50 | "jest": { 51 | "preset": "react-native" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/screens/MainScreen/buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TouchableOpacity } from 'react-native'; 3 | import { BACKGROUND_PRIMARY, COLOR_PRIMARY, COLOR_SECONDARY, COLOR_TERTIARY } from '../../theme'; 4 | 5 | interface ButtonProps { 6 | label: string; 7 | onPress: any; 8 | } 9 | 10 | interface RadioButtonProps extends ButtonProps 11 | { 12 | disabled?: boolean; 13 | } 14 | 15 | export const RoundButton = ({ label, onPress, disabled = false }: RadioButtonProps) => ( 16 | 30 | 35 | {label} 36 | 37 | 38 | ); 39 | 40 | export const IconButton = ({ label, onPress }: ButtonProps) => ( 41 | 51 | 52 | Current VPN server: 53 | 54 | 60 | {label} 61 | 62 | 63 | ); 64 | -------------------------------------------------------------------------------- /ios/ZudVPNTests/ZudVPNTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface ZudVPNTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation ZudVPNTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /ios/ZudVPN/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | #ifdef FB_SONARKIT_ENABLED 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | static void InitializeFlipper(UIApplication *application) { 16 | FlipperClient *client = [FlipperClient sharedClient]; 17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 20 | [client addPlugin:[FlipperKitReactPlugin new]]; 21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 22 | [client start]; 23 | } 24 | #endif 25 | 26 | @implementation AppDelegate 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 29 | { 30 | #ifdef FB_SONARKIT_ENABLED 31 | InitializeFlipper(application); 32 | #endif 33 | 34 | NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 35 | 36 | [[RCTBundleURLProvider sharedSettings] setJsLocation:jsCodeLocation.host]; 37 | 38 | [ReactNativeNavigation bootstrap:jsCodeLocation launchOptions:launchOptions]; 39 | 40 | self.window.backgroundColor = [UIColor blackColor]; 41 | 42 | return YES; 43 | } 44 | 45 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options 46 | { 47 | return [RCTLinkingManager application:application openURL:url options:options]; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /src/screens/MainScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { RoundButton } from './buttons'; 4 | import { useStore } from '../../store/store'; 5 | import useScreen from '../useScreen'; 6 | import AcceptPrivacy from './AcceptPrivacy'; 7 | import Layout from './layout'; 8 | import withInitState from '../../store/withInitState'; 9 | import Notifications from './notifications'; 10 | import CurrentServer from './current_server'; 11 | 12 | const MainScreen = () => { 13 | const [{ privacyAccepted, providerTokens, currentServer, vpnStatus }, { toggleVPN }] = useStore(); 14 | const { SettingsScreenModal } = useScreen(); 15 | 16 | if (!privacyAccepted) { 17 | return ; 18 | } 19 | 20 | if (providerTokens.length === 0) { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | let disabled = vpnStatus === 'Connecting' || vpnStatus === 'Disconnecting'; 32 | 33 | let buttonLabel = vpnStatus; 34 | switch (vpnStatus) { 35 | case 'Connected': 36 | buttonLabel = 'Disconnect'; 37 | break; 38 | case 'Disconnected': 39 | buttonLabel = 'Connect'; 40 | break; 41 | } 42 | 43 | const toggleVPNOrSettingsScreenModal = () => { 44 | if (currentServer) { 45 | toggleVPN(); 46 | } else { 47 | SettingsScreenModal(); 48 | } 49 | }; 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default withInitState(MainScreen); 65 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | module.file_ext=.js 34 | module.file_ext=.json 35 | module.file_ext=.ios.js 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 40 | module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 41 | 42 | suppress_type=$FlowIssue 43 | suppress_type=$FlowFixMe 44 | suppress_type=$FlowFixMeProps 45 | suppress_type=$FlowFixMeState 46 | 47 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 50 | 51 | [lints] 52 | sketchy-null-number=warn 53 | sketchy-null-mixed=warn 54 | sketchy-number=warn 55 | untyped-type-import=warn 56 | nonstrict-import=warn 57 | deprecated-type=warn 58 | unsafe-getters-setters=warn 59 | inexact-spread=warn 60 | unnecessary-invariant=warn 61 | signature-verification-failure=warn 62 | deprecated-utility=error 63 | 64 | [strict] 65 | deprecated-type 66 | nonstrict-import 67 | sketchy-null 68 | unclear-type 69 | unsafe-getters-setters 70 | untyped-import 71 | untyped-type-import 72 | 73 | [version] 74 | ^0.122.0 75 | -------------------------------------------------------------------------------- /src/ssh/client.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // @ts-ignore 4 | import NativeSSHClient from 'react-native-ssh-sftp'; 5 | import { Keypair } from 'ssh/keygen'; 6 | 7 | class Client { 8 | keypair: Keypair; 9 | user: string; 10 | host: string; 11 | port: number; 12 | nativeSshClient: NativeSSHClient; 13 | 14 | constructor(keypair: Keypair, user: string, host: string, port: number) { 15 | this.keypair = keypair; 16 | this.user = user; 17 | this.host = host; 18 | this.port = port; 19 | } 20 | 21 | async openSession() { 22 | if (!this.nativeSshClient) { 23 | return new Promise((resolve, reject) => { 24 | this.nativeSshClient = new NativeSSHClient( 25 | this.host, 26 | this.port, 27 | this.user, 28 | { 29 | privateKey: this.keypair.privateKey, 30 | publicKey: this.keypair.authorizedKey, 31 | }, 32 | (error: any) => { 33 | if (error) { 34 | console.log('An error occurred while establishing SSH connection:', error); 35 | this.nativeSshClient = null; 36 | reject(error); 37 | } else { 38 | resolve(true); 39 | } 40 | }, 41 | ); 42 | }); 43 | } else { 44 | console.log('SSH is open, reusing.'); 45 | } 46 | } 47 | 48 | async run(command: string): Promise { 49 | await this.openSession(); 50 | 51 | return new Promise((resolve, reject) => { 52 | this.nativeSshClient.execute(command, (error: any, output: any) => { 53 | if (error) { 54 | console.log('An error occured while executing command: ', command, error); 55 | reject(error); 56 | } else { 57 | resolve(output); 58 | } 59 | }); 60 | }); 61 | } 62 | 63 | closeSession(): void { 64 | if (this.nativeSshClient) { 65 | this.nativeSshClient.disconnect(); 66 | } 67 | } 68 | } 69 | 70 | export default Client; 71 | -------------------------------------------------------------------------------- /assets/terminal/xterm-addon-fit.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0;var n=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core,t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),n=Math.max(0,parseInt(t.getPropertyValue("width"))),o=window.getComputedStyle(this._terminal.element),i=r-(parseInt(o.getPropertyValue("padding-top"))+parseInt(o.getPropertyValue("padding-bottom"))),a=n-(parseInt(o.getPropertyValue("padding-right"))+parseInt(o.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(i/e._renderService.dimensions.actualCellHeight))}}},e}();t.FitAddon=n}])})); 2 | //# sourceMappingURL=xterm-addon-fit.js.map -------------------------------------------------------------------------------- /src/screens/SettingsScreen/LogViewerScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { SafeAreaView, ScrollView, ActivityIndicator, Alert, TextInput } from 'react-native'; 3 | import RNFS from 'react-native-fs'; 4 | import { APPLICATION_LOG_FILENAME } from '../../../logger'; 5 | import { Navigation } from 'react-native-navigation'; 6 | import { BACKGROUND_PRIMARY, COLOR_SECONDARY } from '../../../theme'; 7 | 8 | const LogFileViewerScreen = (props: any) => { 9 | const [logs, setLogs] = useState(null); 10 | const logFile = RNFS.DocumentDirectoryPath + '/' + APPLICATION_LOG_FILENAME + '.txt'; 11 | 12 | Navigation.events().registerNavigationButtonPressedListener(({ buttonId, componentId }) => { 13 | if (componentId === props.componentId && buttonId === 'clear_log') { 14 | Alert.alert('Warning!', 'Are you sure you want to clear application logs?', [ 15 | { 16 | text: 'Clear', 17 | onPress: () => clearLogs(), 18 | style: 'destructive', 19 | }, 20 | { 21 | text: 'Cancel', 22 | style: 'cancel', 23 | }, 24 | ]); 25 | } 26 | }); 27 | 28 | const clearLogs = () => { 29 | RNFS.writeFile(logFile, '').then(() => { 30 | setLogs(''); 31 | }); 32 | }; 33 | 34 | useEffect(() => { 35 | const read_log_file = async () => { 36 | try { 37 | setLogs(await RNFS.readFile(logFile)); 38 | } catch (e) { 39 | setLogs(''); 40 | } 41 | }; 42 | 43 | read_log_file(); 44 | // eslint-disable-next-line react-hooks/exhaustive-deps 45 | }, []); 46 | 47 | if (logs === null) { 48 | return ( 49 | 56 | 57 | 58 | ); 59 | } 60 | 61 | return ( 62 | 63 | 64 | 65 | {logs} 66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default LogFileViewerScreen; 73 | -------------------------------------------------------------------------------- /ios/ZudVPN/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ZudVPN 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLName 29 | com.zudvpn.app 30 | CFBundleURLSchemes 31 | 32 | zudvpnapp 33 | 34 | 35 | 36 | CFBundleVersion 37 | $(CURRENT_PROJECT_VERSION) 38 | LSRequiresIPhoneOS 39 | 40 | NSAppTransportSecurity 41 | 42 | NSAllowsArbitraryLoads 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | NSLocationWhenInUseUsageDescription 54 | 55 | UIAppFonts 56 | 57 | AntDesign.ttf 58 | Entypo.ttf 59 | EvilIcons.ttf 60 | Feather.ttf 61 | FontAwesome.ttf 62 | FontAwesome5_Brands.ttf 63 | FontAwesome5_Regular.ttf 64 | FontAwesome5_Solid.ttf 65 | Fontisto.ttf 66 | Foundation.ttf 67 | Ionicons.ttf 68 | MaterialCommunityIcons.ttf 69 | MaterialIcons.ttf 70 | Octicons.ttf 71 | SimpleLineIcons.ttf 72 | Zocial.ttf 73 | 74 | UILaunchStoryboardName 75 | LaunchScreen 76 | UIRequiredDeviceCapabilities 77 | 78 | armv7 79 | 80 | UISupportedInterfaceOrientations 81 | 82 | UIInterfaceOrientationPortrait 83 | 84 | UIViewControllerBasedStatusBarAppearance 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/zudvpn/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.zudvpn; 2 | 3 | import android.app.Application; 4 | 5 | import android.content.Context; 6 | import com.facebook.react.PackageList; 7 | import com.facebook.react.ReactApplication; 8 | import com.oblador.vectoricons.VectorIconsPackage; 9 | import com.facebook.react.ReactNativeHost; 10 | import com.facebook.react.ReactPackage; 11 | import com.facebook.soloader.SoLoader; 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 48 | } 49 | /** 50 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 51 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 52 | * 53 | * @param context 54 | * @param reactInstanceManager 55 | */ 56 | private static void initializeFlipper( 57 | Context context, ReactInstanceManager reactInstanceManager) { 58 | if (BuildConfig.DEBUG) { 59 | try { 60 | /* 61 | We use reflection here to pick up the class that initializes Flipper, 62 | since Flipper library is not available in release mode 63 | */ 64 | Class aClass = Class.forName("com.rndiffapp.ReactNativeFlipper"); 65 | aClass 66 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 67 | .invoke(null, context, reactInstanceManager); 68 | } catch (ClassNotFoundException e) { 69 | e.printStackTrace(); 70 | } catch (NoSuchMethodException e) { 71 | e.printStackTrace(); 72 | } catch (IllegalAccessException e) { 73 | e.printStackTrace(); 74 | } catch (InvocationTargetException e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/screens/SettingsScreen/provider_list_item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { StyleSheet } from 'react-native'; 3 | import { ListItem } from 'react-native-elements'; 4 | import { useStore } from '../../store/store'; 5 | import useScreen from '../useScreen'; 6 | import { BACKGROUND_SECONDARY, COLOR_SECONDARY } from '../../theme'; 7 | import { Provider } from 'providers/types/Provider'; 8 | 9 | interface Props { 10 | item: { id: string; name: string; available: boolean }; 11 | componentId: string; 12 | } 13 | 14 | export const ProviderListItem = ({ item, componentId }: Props) => { 15 | const [{ providerTokens }] = useStore(); 16 | const { ProviderRegisterScreenPush, ProviderRegionScreenPush } = useScreen(); 17 | 18 | const getAccount = (provider: Provider) => { 19 | const token = providerTokens.filter((t) => t.provider === provider.id); 20 | 21 | if (token.length > 0) { 22 | return `connected as ${token[0].account?.email}`; 23 | } 24 | 25 | return null; 26 | }; 27 | 28 | const onPress = (provider: Provider) => { 29 | const token = providerTokens.filter((t) => t.provider === provider.id); 30 | 31 | if (token.length > 0) { 32 | ProviderRegionScreenPush(componentId, provider); 33 | } else { 34 | ProviderRegisterScreenPush({ componentId, provider }); 35 | } 36 | }; 37 | 38 | if (item.available) { 39 | const account = getAccount(item); 40 | 41 | return ( 42 | onPress(item)}> 46 | 47 | {item.name} 48 | {account && ( 49 | 50 | {account} 51 | 52 | )} 53 | 54 | 55 | 56 | ); 57 | } 58 | 59 | return <>; 60 | 61 | // return ( 62 | // 63 | // 64 | // {item.name} 65 | // 66 | // 67 | // 68 | // {'coming soon'} 69 | // 70 | // 71 | // 72 | // ); 73 | }; 74 | 75 | // const styles = StyleSheet.create({ 76 | // disabled: { 77 | // opacity: 0.7, 78 | // }, 79 | // }); 80 | -------------------------------------------------------------------------------- /src/providers/DigitalOcean/ClientFacade.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { SERVER_TAG } from './constants'; 4 | import Deploy from './Deploy'; 5 | import ApiClient from './ApiClient'; 6 | import Keychain from '../../keychain'; 7 | import { Client } from 'providers/types/Client'; 8 | import { Region } from 'providers/types/Region'; 9 | import { Notify } from 'store/types/Notify'; 10 | import { Server } from 'providers/types/Server'; 11 | import { VPNCredentials } from 'providers/types/VPNCredentials'; 12 | 13 | class ClientFacade implements Client { 14 | token: string; 15 | apiClient: ApiClient; 16 | 17 | constructor(token: string) { 18 | this.token = token; 19 | 20 | this.apiClient = new ApiClient(token); 21 | } 22 | 23 | async getAccount() { 24 | return await this.apiClient.getAccount(); 25 | } 26 | 27 | async createServer(region: Region, notify: Notify): Promise { 28 | const deploy = new Deploy(this.apiClient, notify); 29 | 30 | return await deploy.run(region.slug); 31 | } 32 | 33 | async readServerVPN(server: Server, notify: Notify): Promise { 34 | const deploy = new Deploy(this.apiClient, notify); 35 | 36 | const sshKeyPair = await Keychain.getSSHKeyPair(server.name); 37 | 38 | return await deploy.read(server, sshKeyPair); 39 | } 40 | 41 | async getServers(): Promise { 42 | let droplets = await this.apiClient.getDropletsByTag(SERVER_TAG); 43 | 44 | return droplets.map( 45 | (droplet: any): Server => { 46 | const ipAddress = droplet.networks.v4.filter((ip: any) => ip.type === 'public'); 47 | 48 | return { 49 | provider: { 50 | id: 'digitalocean', 51 | name: 'DigitalOcean', 52 | }, 53 | uid: droplet.id, 54 | name: droplet.name, 55 | region: { 56 | name: droplet.region.name, 57 | slug: droplet.region.slug, 58 | available: droplet.region.available, 59 | }, 60 | ipv4Address: ipAddress[0].ip_address, 61 | }; 62 | }, 63 | ); 64 | } 65 | 66 | async deleteServer(server: Server): Promise { 67 | this.apiClient.deleteDroplet(server.uid); 68 | const sshKeyPair = await Keychain.getSSHKeyPair(server.name); 69 | this.apiClient.deleteSshKey(sshKeyPair.fingerprint); 70 | } 71 | 72 | async getRegions(): Promise { 73 | let regions = await this.apiClient.getRegions(); 74 | 75 | return regions.map( 76 | (region: any): Region => { 77 | return { 78 | name: region.name, 79 | slug: region.slug, 80 | available: region.available, 81 | }; 82 | }, 83 | ); 84 | } 85 | } 86 | 87 | export default ClientFacade; 88 | -------------------------------------------------------------------------------- /src/screens/SettingsScreen/ProviderRegionScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ActivityIndicator, Alert, FlatList, SafeAreaView, ScrollView, Text } from 'react-native'; 3 | import RegionListItem from './region_list_item'; 4 | import withClient from '../../../providers/with_client'; 5 | import { Navigation } from 'react-native-navigation'; 6 | import { useStore } from '../../../store/store'; 7 | import { BACKGROUND_PRIMARY, COLOR_SECONDARY } from '../../../theme'; 8 | 9 | const ProviderRegionScreen = (props: any) => { 10 | const [regions, setRegions] = useState(null); 11 | const [, { triggerSignOut }] = useStore(); 12 | 13 | Navigation.events().registerNavigationButtonPressedListener(({ buttonId, componentId }) => { 14 | if (componentId === props.componentId && buttonId === 'sign_out') { 15 | Alert.alert('Warning!', `Are you sure you want to sign out of ${props.provider.name}?`, [ 16 | { 17 | text: 'Sign out', 18 | onPress: () => { 19 | triggerSignOut(props.provider); 20 | Navigation.pop(props.componentId); 21 | }, 22 | style: 'destructive', 23 | }, 24 | { 25 | text: 'Cancel', 26 | style: 'cancel', 27 | }, 28 | ]); 29 | } 30 | }); 31 | 32 | const retrieveRegions = async () => { 33 | const _regions = await props.client.getRegions(props.provider.id); 34 | 35 | setRegions(_regions); 36 | }; 37 | 38 | if (regions === null) { 39 | retrieveRegions(); 40 | 41 | return ( 42 | 49 | 50 | 51 | ); 52 | } 53 | 54 | return ( 55 | 56 | 64 | Regions on {props.provider.name} 65 | 66 | 67 | Select a region to deploy a VPN server on 68 | 69 | } 72 | keyExtractor={(item, index) => index.toString()} 73 | /> 74 | 75 | ); 76 | }; 77 | 78 | export default withClient(ProviderRegionScreen); 79 | -------------------------------------------------------------------------------- /src/screens/SettingsScreen/ProviderRegionScreen/region_list_item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, StyleSheet } from 'react-native'; 3 | import { ListItem } from 'react-native-elements'; 4 | import { Navigation } from 'react-native-navigation'; 5 | import { useStore } from '../../../store/store'; 6 | import withClient from '../../../providers/with_client'; 7 | import { BACKGROUND_SECONDARY, COLOR_SECONDARY } from '../../../theme'; 8 | import { Region } from 'providers/types/Region'; 9 | import { Provider } from 'providers/types/Provider'; 10 | import Client from 'providers/client'; 11 | 12 | interface Props { 13 | item: Region; 14 | provider: Provider; 15 | client: Client; 16 | } 17 | 18 | const RegionListItem = ({ item, provider, client }: Props) => { 19 | const [, { setCurrentVPNServer, setVPNStatus, notify }] = useStore(); 20 | 21 | const createServer = (region: Region) => { 22 | Navigation.dismissAllModals(); 23 | 24 | setTimeout(async () => { 25 | try { 26 | setVPNStatus('Connecting'); 27 | 28 | const server = await client.createServer(provider.id, region, notify); 29 | setCurrentVPNServer(server); 30 | 31 | notify('success', 'Voila! VPN server is ready for connection.'); 32 | } catch (e) { 33 | setVPNStatus('Connect'); 34 | notify('error', `Failed to create VPN Server: ${e.message || JSON.stringify(e)}`); 35 | } 36 | }, 500); 37 | }; 38 | 39 | const confirmCreateServer = (region: Region) => { 40 | Alert.alert('Confirm', `This will create a VPN server on "${region.name}" region on ${provider.name}`, [ 41 | { 42 | text: 'Proceed', 43 | onPress: () => createServer(region), 44 | }, 45 | { 46 | text: 'Cancel', 47 | style: 'cancel', 48 | }, 49 | ]); 50 | }; 51 | 52 | if (item.available) { 53 | return ( 54 | confirmCreateServer(item)}> 58 | 59 | {item.name} 60 | {item.slug} 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | return ( 68 | 69 | 70 | {item.name} 71 | {item.slug} 72 | 73 | 74 | 75 | {'unavailable'} 76 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default withClient(RegionListItem); 84 | 85 | const styles = StyleSheet.create({ 86 | disabled: { 87 | opacity: 0.7, 88 | }, 89 | }); 90 | -------------------------------------------------------------------------------- /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 init 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 init 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 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /src/store/store.tsx: -------------------------------------------------------------------------------- 1 | import { createStore, createHook, SetState, GetState } from 'react-sweet-state'; 2 | // @ts-ignore 3 | import RNNetworkExtension from 'react-native-network-extension'; 4 | import logger from '../logger'; 5 | import Keychain from '../keychain'; 6 | import { Token } from 'providers/types/Token'; 7 | import { Server } from 'providers/types/Server'; 8 | import { Provider } from 'providers/types/Provider'; 9 | 10 | interface State { 11 | privacyAccepted: boolean; 12 | providerTokens: Token[]; 13 | currentServer: Server | null; 14 | vpnStatus: string; 15 | notifications: { type: string; notification: string }[]; 16 | } 17 | 18 | const initialState: State = { 19 | privacyAccepted: false, 20 | providerTokens: [], 21 | currentServer: null, 22 | vpnStatus: 'Disconnected', 23 | notifications: [], 24 | }; 25 | 26 | interface StoreActions { 27 | setState: SetState; 28 | getState: GetState; 29 | dispatch: any; 30 | } 31 | 32 | const actions = { 33 | acceptPrivacy: () => ({ setState, dispatch }: StoreActions) => { 34 | setState({ privacyAccepted: true }); 35 | 36 | dispatch(actions.persistState()); 37 | }, 38 | addProviderToken: (token: Token) => ({ setState, getState, dispatch }: StoreActions) => { 39 | setState({ providerTokens: [...getState().providerTokens, token] }); 40 | 41 | dispatch(actions.persistState()); 42 | }, 43 | setCurrentVPNServer: (server: Server | null) => ({ setState, dispatch }: StoreActions) => { 44 | setState({ currentServer: server }); 45 | 46 | dispatch(actions.persistState()); 47 | }, 48 | setVPNStatus: (status: string) => ({ setState }: StoreActions) => { 49 | setState({ vpnStatus: status }); 50 | }, 51 | toggleVPN: () => async ({ setState, getState, dispatch }: StoreActions) => { 52 | if (getState().vpnStatus === 'Connected') { 53 | RNNetworkExtension.disconnect(); 54 | } else { 55 | setState({ vpnStatus: 'Connecting' }); 56 | 57 | try { 58 | await RNNetworkExtension.connect(); 59 | } catch (e) { 60 | dispatch(actions.notify('error', `Failed to start VPN connection: ${e.message || JSON.stringify(e)}`)); 61 | } 62 | } 63 | }, 64 | triggerSignOut: (provider: Provider) => ({ setState, getState, dispatch }: StoreActions) => { 65 | setState({ 66 | providerTokens: getState().providerTokens.filter((token: Token) => token.provider !== provider.id), 67 | }); 68 | 69 | dispatch(actions.persistState()); 70 | }, 71 | resetNotification: () => ({ setState }: StoreActions) => { 72 | setState({ notifications: [] }); 73 | }, 74 | notify: (type: string, notification: string) => ({ setState, getState }: StoreActions) => { 75 | setState({ notifications: [{ type, notification }, ...getState().notifications] }); 76 | 77 | logger.log(type, notification); 78 | }, 79 | initState: (state: State) => ({ setState }: StoreActions) => setState(state), 80 | persistState: () => ({ getState }: StoreActions) => { 81 | const state = { 82 | privacyAccepted: getState().privacyAccepted, 83 | providerTokens: getState().providerTokens, 84 | currentServer: getState().currentServer, 85 | }; 86 | 87 | Keychain.setInitialState(state); 88 | }, 89 | }; 90 | 91 | const Store = createStore({ 92 | name: 'main_store', 93 | initialState, 94 | actions, 95 | }); 96 | 97 | export const useStore = createHook(Store); 98 | -------------------------------------------------------------------------------- /ios/ZudVPN.xcodeproj/xcshareddata/xcschemes/ZudVPN.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/ZudVPN.xcodeproj/xcshareddata/xcschemes/ZudVPN-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/providers/client.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import DO_Client from './DigitalOcean/ClientFacade'; 4 | // @ts-ignore 5 | import RNNetworkExtension from 'react-native-network-extension'; 6 | import logger from '../logger'; 7 | import { Token } from 'providers/types/Token'; 8 | import { Client as ClientInterface } from 'providers/types/Client'; 9 | import { Region } from 'providers/types/Region'; 10 | import { Notify } from 'store/types/Notify'; 11 | import { Server } from 'providers/types/Server'; 12 | 13 | class Client { 14 | clients: Map; 15 | 16 | constructor(tokens: Array) { 17 | this.clients = new Map(); 18 | 19 | for (const token of tokens) { 20 | this.clients.set(token.provider, this.createClient(token)); 21 | } 22 | } 23 | 24 | createClient(token: Token): ClientInterface { 25 | if (token.provider === 'digitalocean') { 26 | return new DO_Client(token.accessToken); 27 | } 28 | 29 | throw new Error('Token provider is not registered.'); 30 | } 31 | 32 | async createServer(provider: string, region: Region, notify: Notify): Promise { 33 | const vpnCredentials = await this.clients.get(provider)?.createServer(region, notify); 34 | 35 | if (!vpnCredentials) { 36 | throw new Error('Cannot retrieve VPN credentials'); 37 | } 38 | 39 | notify('progress', 'Configuring authentication'); 40 | await RNNetworkExtension.configure({ 41 | ipAddress: vpnCredentials.ipAddress, 42 | domain: vpnCredentials.domain, 43 | username: vpnCredentials.username, 44 | password: vpnCredentials.password, 45 | }); 46 | 47 | return vpnCredentials.server; 48 | } 49 | 50 | async configureServer(provider: string, server: Server, notify: Notify): Promise { 51 | const vpnCredentials = await this.clients.get(provider)?.readServerVPN(server, notify); 52 | 53 | if (!vpnCredentials) { 54 | throw new Error('Cannot retrieve VPN credentials'); 55 | } 56 | 57 | notify('progress', 'Configuring authentication'); 58 | await RNNetworkExtension.configure({ 59 | ipAddress: vpnCredentials.ipAddress, 60 | domain: vpnCredentials.domain, 61 | username: vpnCredentials.username, 62 | password: vpnCredentials.password, 63 | }); 64 | 65 | return vpnCredentials.server; 66 | } 67 | 68 | async getRegions(provider: string) { 69 | try { 70 | return await this.clients.get(provider)?.getRegions(); 71 | } catch (e) { 72 | logger.warn([`Cannot load ${provider} regions`, e.message]); 73 | } 74 | 75 | return []; 76 | } 77 | 78 | async getServers(): Promise { 79 | const requests = Array.from(this.clients, ([, client]) => client.getServers().catch((e) => e)); 80 | 81 | const responses = await Promise.all(requests); 82 | 83 | return responses 84 | .filter((response: Server | Error) => { 85 | if (response instanceof Error) { 86 | logger.warn('Retrieving servers failed, reason: ' + response.message); 87 | 88 | return false; 89 | } 90 | 91 | return true; 92 | }) 93 | .flat(); 94 | } 95 | 96 | async deleteServer(server: Server): Promise { 97 | this.clients.get(server.provider.id)?.deleteServer(server); 98 | } 99 | } 100 | 101 | export default Client; 102 | -------------------------------------------------------------------------------- /src/screens/MainScreen/linking_listener.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { parse_linking_url_token } from '../../helper'; 3 | import { Linking, Platform } from 'react-native'; 4 | // @ts-ignore 5 | import RNNetworkExtension from 'react-native-network-extension'; 6 | import { useStore } from '../../store/store'; 7 | import StaticServer from '../../static_server'; 8 | import { Navigation } from 'react-native-navigation'; 9 | // @ts-ignore 10 | import SafariView from 'react-native-safari-view'; 11 | import withClient from '../../providers/with_client'; 12 | import logger from '../../logger'; 13 | 14 | const LinkingListener = (props: any) => { 15 | const [, { addProviderToken, setVPNStatus, notify, resetNotification }] = useStore(); 16 | 17 | useEffect(() => { 18 | const networkStatusCallback = (status: string) => { 19 | logger.debug('Network status: ' + status); 20 | setVPNStatus(status); 21 | }; 22 | 23 | const networkFailCallback = (reason: string) => { 24 | logger.debug('Network failed, reason: ' + reason); 25 | setVPNStatus('Connect'); 26 | }; 27 | 28 | const handleCallback = async (url: string) => { 29 | // Reset/remove previous notifications from main screen. 30 | resetNotification(); 31 | const providerToken = parse_linking_url_token(url); 32 | 33 | if (providerToken) { 34 | const client = props.client.createClient({ 35 | provider: providerToken.provider, 36 | accessToken: providerToken.accessToken, 37 | }); 38 | 39 | try { 40 | providerToken.account = await client.getAccount(); 41 | addProviderToken(providerToken); 42 | } catch (e) { 43 | notify('error', 'Cannot add provider token: ' + e.message); 44 | } 45 | } else { 46 | notify('error', 'Cannot add provider token: Missing access token.'); 47 | } 48 | 49 | // We assume that after receiving callback url from Provider registration/login page 50 | // our static server, navigation modal and safari view are still open, 51 | // thus we programmatically return user to main screen. 52 | StaticServer.stop(); 53 | Navigation.dismissAllModals(); 54 | SafariView.dismiss(); 55 | }; 56 | 57 | const handleCallbackEvent = (event: any) => { 58 | handleCallback(event.url); 59 | }; 60 | 61 | if (Platform.OS === 'android') { 62 | Linking.getInitialURL().then((url) => { 63 | handleCallback(url as string); 64 | }); 65 | } else { 66 | Linking.addEventListener('url', handleCallbackEvent); 67 | } 68 | 69 | const vpnStatusListener = RNNetworkExtension.addEventListener('status', networkStatusCallback); 70 | 71 | const vpnFailListener = RNNetworkExtension.addEventListener('fail', networkFailCallback); 72 | 73 | // remove listeners on component unmount 74 | return () => { 75 | Linking.removeEventListener('url', handleCallbackEvent); 76 | 77 | vpnStatusListener.remove(); 78 | RNNetworkExtension.removeEventListener('status', networkStatusCallback); 79 | 80 | vpnFailListener.remove(); 81 | RNNetworkExtension.removeEventListener('fail', networkFailCallback); 82 | }; 83 | // eslint-disable-next-line react-hooks/exhaustive-deps 84 | }, []); 85 | 86 | return props.children; 87 | }; 88 | 89 | export default withClient(LinkingListener); 90 | -------------------------------------------------------------------------------- /docs/REINSTALL.md: -------------------------------------------------------------------------------- 1 | REINSTALL GUIDE 2 | --- 3 | The world of mobile application development changes frequently. 4 | To keep up-to-date, applications must continuously update their dependent packages. 5 | The easiest way, we believe, is to periodically reinstall ZudVPN from scratch. 6 | Installing from scratch means create a new react-native project and install every dependent package individually. 7 | 8 | 1. Let's begin by creating a new react-native project. 9 | ``` 10 | npx react-native init ZudVPN 11 | // for tvOS, init with template 12 | npx react-native init ZudVPN --template=react-native-tvos@latest 13 | ``` 14 | 2. ZudVPN is written in TypeScript to ensure type safety. Copy the following `tsconfig.json` to root folder. 15 | ```json 16 | { 17 | "compilerOptions": { 18 | "allowJs": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true, 21 | "isolatedModules": true, 22 | "jsx": "react", 23 | "lib": ["esnext"], 24 | "moduleResolution": "node", 25 | "noEmit": true, 26 | "strict": true, 27 | "target": "esnext", 28 | "baseUrl": ".", 29 | "paths": { 30 | "*": ["src/*"], 31 | "tests": ["tests/*"] 32 | }, 33 | "skipLibCheck": true, 34 | }, 35 | "exclude": [ 36 | "node_modules", 37 | "babel.config.js", 38 | "metro.config.js", 39 | "jest.config.js" 40 | ] 41 | } 42 | ``` 43 | 3. Create `jest.config.js` file to configure Jest to use TypeScript. 44 | ```javascript 45 | module.exports = { 46 | preset: 'react-native', 47 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 48 | }; 49 | ``` 50 | 4. Add Typescript dependencies using `yarn`. 51 | ``` 52 | yarn add -D typescript @types/jest @types/react @types/react-native @types/react-test-renderer 53 | ``` 54 | 5. Copy `index.js` and `src` file and folders. 55 | 6. Install `react-native-navigation` by following instructions at [https://wix.github.io/react-native-navigation/docs/installing](https://wix.github.io/react-native-navigation/docs/installing) 56 | ``` 57 | yarn add react-native-navigation 58 | ``` 59 | 7. Install required packages 60 | ``` 61 | yarn add react-sweet-state # handles stateful components, avoids redux action repetitiveness 62 | yarn add react-native-logs # a simple, performance-aware logger for app logs 63 | 64 | yarn add react-native-vector-icons # provides collections of icons 65 | yarn add react-native-elements # provides custom UI elements 66 | 67 | yarn add react-native-fs # creates local files for app logs, html for oauth 68 | yarn add react-native-static-server # starts static server to redirect Oauth2 callback to app linking (`zudvpn://`) domain 69 | yarn add react-native-safari-view # displays web page using native safari browser 70 | 71 | yarn add react-native-network-extension # interacts with ios network api 72 | 73 | yarn add react-native-ssh-sftp # connects to VPN server using SSH protocol 74 | yarn add uuid react-native-get-random-values # generates uuid using crypto random values polyfill 75 | yarn add react-native-rsa-native node-forge # generates ssh keys (pub/priv) 76 | yarn add react-native-keychain # stores server credentials securely 77 | 78 | 79 | yarn add react-native-webview # renders terminal as a webpage 80 | yarn add xterm xterm-addon-fit # a fully-featured terminal 81 | ``` 82 | 8. Run the following to enable installed libraries for iOS 83 | ``` 84 | npx pod-install 85 | ``` 86 | -------------------------------------------------------------------------------- /ios/ZudVPN/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /assets/terminal/xterm.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * @license MIT 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | * Originally forked from (with the author's permission): 26 | * Fabrice Bellard's javascript vt100 for jslinux: 27 | * http://bellard.org/jslinux/ 28 | * Copyright (c) 2011 Fabrice Bellard 29 | * The original design remains. The terminal itself 30 | * has been extended to include xterm CSI codes, among 31 | * other features. 32 | */ 33 | 34 | /** 35 | * Default styles for xterm.js 36 | */ 37 | 38 | .xterm { 39 | font-feature-settings: "liga" 0; 40 | position: relative; 41 | user-select: none; 42 | -ms-user-select: none; 43 | -webkit-user-select: none; 44 | } 45 | 46 | .xterm.focus, 47 | .xterm:focus { 48 | outline: none; 49 | } 50 | 51 | .xterm .xterm-helpers { 52 | position: absolute; 53 | top: 0; 54 | /** 55 | * The z-index of the helpers must be higher than the canvases in order for 56 | * IMEs to appear on top. 57 | */ 58 | z-index: 5; 59 | } 60 | 61 | .xterm .xterm-helper-textarea { 62 | /* 63 | * HACK: to fix IE's blinking cursor 64 | * Move textarea out of the screen to the far left, so that the cursor is not visible. 65 | */ 66 | position: absolute; 67 | opacity: 0; 68 | left: -9999em; 69 | top: 0; 70 | width: 0; 71 | height: 0; 72 | z-index: -5; 73 | /** Prevent wrapping so the IME appears against the textarea at the correct position */ 74 | white-space: nowrap; 75 | overflow: hidden; 76 | resize: none; 77 | } 78 | 79 | .xterm .composition-view { 80 | /* TODO: Composition position got messed up somewhere */ 81 | background: #000; 82 | color: #FFF; 83 | display: none; 84 | position: absolute; 85 | white-space: nowrap; 86 | z-index: 1; 87 | } 88 | 89 | .xterm .composition-view.active { 90 | display: block; 91 | } 92 | 93 | .xterm .xterm-viewport { 94 | /* On OS X this is required in order for the scroll bar to appear fully opaque */ 95 | background-color: #000; 96 | overflow-y: scroll; 97 | cursor: default; 98 | position: absolute; 99 | right: 0; 100 | left: 0; 101 | top: 0; 102 | bottom: 0; 103 | } 104 | 105 | .xterm .xterm-screen { 106 | position: relative; 107 | } 108 | 109 | .xterm .xterm-screen canvas { 110 | position: absolute; 111 | left: 0; 112 | top: 0; 113 | } 114 | 115 | .xterm .xterm-scroll-area { 116 | visibility: hidden; 117 | } 118 | 119 | .xterm-char-measure-element { 120 | display: inline-block; 121 | visibility: hidden; 122 | position: absolute; 123 | top: 0; 124 | left: -9999em; 125 | line-height: normal; 126 | } 127 | 128 | .xterm { 129 | cursor: text; 130 | } 131 | 132 | .xterm.enable-mouse-events { 133 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ 134 | cursor: default; 135 | } 136 | 137 | .xterm.xterm-cursor-pointer { 138 | cursor: pointer; 139 | } 140 | 141 | .xterm.column-select.focus { 142 | /* Column selection mode */ 143 | cursor: crosshair; 144 | } 145 | 146 | .xterm .xterm-accessibility, 147 | .xterm .xterm-message { 148 | position: absolute; 149 | left: 0; 150 | top: 0; 151 | bottom: 0; 152 | right: 0; 153 | z-index: 10; 154 | color: transparent; 155 | } 156 | 157 | .xterm .live-region { 158 | position: absolute; 159 | left: -9999px; 160 | width: 1px; 161 | height: 1px; 162 | overflow: hidden; 163 | } 164 | 165 | .xterm-dim { 166 | opacity: 0.5; 167 | } 168 | 169 | .xterm-underline { 170 | text-decoration: underline; 171 | } 172 | -------------------------------------------------------------------------------- /src/screens/SSHTerminalScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { SafeAreaView, ActivityIndicator } from 'react-native'; 3 | import { Navigation } from 'react-native-navigation'; 4 | // @ts-ignore 5 | import SSHClient from 'react-native-ssh-sftp'; 6 | import Keychain from '../../keychain'; 7 | import WebView from 'react-native-webview'; 8 | import TerminalServer from './terminal_server'; 9 | import logger from '../../logger'; 10 | import { BACKGROUND_PRIMARY } from '../../theme'; 11 | 12 | interface Props { 13 | componentId: string; 14 | name: string; 15 | ipv4Address: string; 16 | } 17 | 18 | interface State { 19 | sshClient: SSHClient | null; 20 | terminalUrl: string | null; 21 | } 22 | 23 | class SSHTerminalScreen extends Component { 24 | webref: any; 25 | 26 | constructor(props: any) { 27 | super(props); 28 | Navigation.events().bindComponent(this); 29 | 30 | this.state = { 31 | sshClient: null, 32 | terminalUrl: null, 33 | }; 34 | } 35 | 36 | navigationButtonPressed({ buttonId }: { buttonId: string }) { 37 | if (buttonId === 'cancel') { 38 | Navigation.dismissModal(this.props.componentId); 39 | } 40 | } 41 | 42 | componentDidMount() { 43 | this.startSSHClient(); 44 | } 45 | 46 | async startSSHClient() { 47 | logger.debug('Starting SSH Client'); 48 | let sshKeyPair = await Keychain.getSSHKeyPair(this.props.name); 49 | 50 | if (!sshKeyPair) { 51 | this.sendMessage('SSH Keypair is not available. Cannot cannot to server terminal.'); 52 | } else { 53 | let sshClient = new SSHClient( 54 | this.props.ipv4Address, 55 | 22, 56 | 'rancher', 57 | { 58 | privateKey: sshKeyPair.privateKey, 59 | publicKey: sshKeyPair.authorizedKey, 60 | }, 61 | (error: any) => { 62 | if (error) { 63 | this.sendMessage(error); 64 | } else { 65 | this.setState({ sshClient }); 66 | 67 | sshClient.startShell('xterm', (shellError: any) => { 68 | if (shellError) { 69 | this.sendMessage(shellError); 70 | } 71 | }); 72 | 73 | sshClient.on('Shell', (event: any) => { 74 | this.sendMessage(event); 75 | }); 76 | } 77 | }, 78 | ); 79 | } 80 | } 81 | 82 | componentWillUnmount() { 83 | let { sshClient } = this.state; 84 | 85 | if (sshClient) { 86 | console.log('SSH client is disconnectiong.'); 87 | sshClient.closeShell(); 88 | sshClient.disconnect(); 89 | } 90 | } 91 | 92 | startTerminalServer = async () => { 93 | const url = await TerminalServer.serveTerminal(); 94 | this.setState({ terminalUrl: url }); 95 | }; 96 | 97 | onMessage = (event: any) => { 98 | let { sshClient } = this.state; 99 | 100 | let command = JSON.parse(event.nativeEvent.data); 101 | if (sshClient) { 102 | sshClient.writeToShell(command, (error: any) => { 103 | if (error) { 104 | this.sendMessage(error); 105 | } 106 | }); 107 | } 108 | }; 109 | 110 | sendMessage = (message: string) => { 111 | if (this.webref) { 112 | message = message.replace(/\n/gi, '\\r'); 113 | this.webref.injectJavaScript(`window.terminal.write(\`${message}\`);true;`); 114 | } 115 | }; 116 | 117 | render() { 118 | let { terminalUrl } = this.state; 119 | 120 | if (terminalUrl === null) { 121 | this.startTerminalServer(); 122 | 123 | return ( 124 | 131 | 132 | 133 | ); 134 | } 135 | 136 | return ( 137 | (this.webref = ref)} 140 | originWhitelist={['*']} 141 | source={{ uri: terminalUrl }} 142 | onMessage={this.onMessage} 143 | onError={(syntheticEvent) => { 144 | const { nativeEvent } = syntheticEvent; 145 | logger.warn('Terminal WebView error:', nativeEvent); 146 | }} 147 | /> 148 | ); 149 | } 150 | } 151 | 152 | export default SSHTerminalScreen; 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ZudVPN 2 | == 3 | A mobile application to deploy a personal VPN server in the cloud (DigitalOcean, AWS, GCP, Azure and others) with DNS ad-blocking and other features 4 | 5 | ![Create your own private VPN using ZudVPN](./screenshots/app.jpeg?raw=true "Create your own private VPN using ZudVPN") 6 | 7 | Features 8 | -- 9 | - Deploys a VPN server to major Cloud Providers (DigitalOcean only, others coming soon). 10 | - Connects to Cloud Providers using OAuth2 or an existing API token. 11 | - Uses IKEv2 IPSec-based VPN service ([strongSwan](https://strongswan.org/)). 12 | - Sets up an ad-blocking DNS resolver ([Pi-hole](https://pi-hole.net/)). 13 | - Installs VPN profile with SSL/TLS certificate (Signed by [Let's Encrypt](https://letsencrypt.org/)). 14 | - Uses native VPN client of iOS (Android version is in development). 15 | - Contains a [xterm.js](https://xtermjs.org/ )-based Terminal for SSH connection to the server (Experimental) 16 | 17 | 18 | How to install? 19 | -- 20 | - The easiest way to install ZudVPN is from App Store or Google Play (soon). 21 | - Apple App Store: https://apps.apple.com/us/app/zudvpn-personal-vpn-on-cloud/id1517610454 22 | - It is also possible to [build from the source](docs/INSTALL.md). 23 | 24 | 25 | How to use? 26 | -- 27 | - Launch the app and connect to a Cloud Provider using OAuth2 or an existing API token. 28 | - Choose your cloud provider and select a region to start the deployment. 29 | - It usually takes 3 minutes (on DigitalOcean) to get the VPN server up and running. 30 | - After the deployment, VPN Profile will be loaded automatically. 31 | - Voila! Start the connection. Now you are behind your personal VPN server. 32 | 33 | 34 | FAQ 35 | -- 36 | 1. What does ZudVPN offer that free/public/private VPN applications don't? 37 | * Although most VPN applications claim that they do not log or track your online activities, do not put a blind faith on them. 38 | * VPN servers created by ZudVPN is only accessible by you. 39 | 2. How is this different from [trailofbits/algo](https://github.com/trailofbits/algo)? 40 | * Installation - ZudVPN works right from your phone to create a VPN server. 41 | 3. How much does it cost? 42 | * ZudVPN uses cheapest cloud servers. For instance, DigitalOcean's cheapest plan costs 5$/month server. 43 | * Besides, you can always destroy the server whenever you don't need. 44 | 4. What is the bandwidth limit? 45 | - That depends on Cloud Providers. In general, the bandwidths are above 1 TB/month. 46 | 5. How does ZudVPN create VPN profiles? 47 | - On iOS, to create a legitimate VPN profile, iOS requires a valid SSL certificate for the VPN server and a Personal VPN entitlements. 48 | ZudVPN generates certificates using [Let's Encrypt](https://letsencrypt.org/). To generate certificates Let's Encrypt requires a valid domain. During the deployment, ZudVPN generates a domain name bound to the IP address of your server. 49 | 6. How do I SSH into the deployed VPN server? 50 | - ZudVPN has an incorporated Terminal feature that you can use to log into your server. (Experimental feature) 51 | 7. What is the password to login to Pi-hole? 52 | - The password is `zudvpn`. Access to Pi-hole is restricted to VPN connected users. 53 | 8. Are you going to support other Cloud Providers? 54 | - Yes, we are working to add more providers. 55 | 9. Will this make me completely anonymous? 56 | - No, absolutely not. All of your traffic is going through a provider which could be traced back to your account. You can still be tracked by browser fingerprinting, etc. Your IP address may still leak due to WebRTC, Flash, etc. 57 | 10. How do I uninstall VPN profiles? 58 | - You can destroy VPN server from within the application. This will automatically delete the VPN profile from your phone as well. However, if you delete the profile manually from iOS VPN settings, the server would still be active. You must destroy the server in order to not get charged by the provider. 59 | 60 | Troubleshoot 61 | -- 62 | - VPN service failures 63 | - Occasionally obtaining SSL certificate from Let's Encrypt may fail. The easiest way to resolve the issue is to destroy and re-create the server. 64 | 65 | Todo 66 | -- 67 | - Finish work on Android version 68 | - Work on tvOS version 69 | - Add AWS, GCP and other cloud providers 70 | - Keychain/Keystore shared VPN 71 | - Evaluate WireGuard as a VPN solution 72 | 73 | Donate 74 | -- 75 | All donations support continued development of ZudVPN. 76 | - We accept donations via [Patreon](https://www.patreon.com/miniyarov). 77 | 78 | Powered by 79 | -- 80 | - [React-Native](https://reactnative.dev/) - A learn once, write everywhere mobile app framework. 81 | - [RancherOS](https://rancher.com/) - A containerized OS. 82 | - [strongSwan](https://strongswan.org/) - IPSec-based VPN solution. 83 | - [Let's Encrypt](https://letsencrypt.org/) - Free SSL certificate provider. 84 | - [Pi-hole](https://pi-hole.net/) - DNS ad-blocker. 85 | 86 | Acknowledgements 87 | -- 88 | - [dan-v/dosxvpn](https://github.com/dan-v/dosxvpn) - ZudVPN is mostly inspired by this project. 89 | - [trailofbits/algo](https://github.com/trailofbits/algo) - strongSwan configuration is borrowed from this project. 90 | 91 | Building from source 92 | -- 93 | - Follow [install steps](docs/INSTALL.md) to build the application locally. 94 | - For iOS: You must have an Apple Developer account because this application uses paid-developer only entitlement Personal VPN. 95 | -------------------------------------------------------------------------------- /src/screens/useScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Navigation, OptionsModalPresentationStyle } from 'react-native-navigation'; 2 | import { 3 | LOG_FILE_VIEWER_SCREEN, 4 | PROVIDER_REGION_SCREEN, 5 | PROVIDER_REGISTER_SCREEN, 6 | SETTINGS_SCREEN, 7 | SSH_TERMINAL_SCREEN, 8 | } from './constants'; 9 | import { Provider } from 'providers/types/Provider'; 10 | 11 | const useScreen = () => { 12 | return { 13 | ProviderRegisterScreenPush: (props: { componentId: string; provider: Provider }) => 14 | Navigation.push(props.componentId, { 15 | component: { 16 | name: PROVIDER_REGISTER_SCREEN, 17 | options: { 18 | topBar: { 19 | title: { 20 | text: props.provider.name, 21 | }, 22 | }, 23 | }, 24 | passProps: props, 25 | }, 26 | }), 27 | ProviderRegionScreenPush: (componentId: string, provider: Provider) => 28 | Navigation.push(componentId, { 29 | component: { 30 | name: PROVIDER_REGION_SCREEN, 31 | options: { 32 | topBar: { 33 | title: { 34 | text: 'Regions', 35 | }, 36 | rightButtons: [ 37 | { 38 | id: 'sign_out', 39 | text: 'Sign out', 40 | color: 'red', 41 | }, 42 | ], 43 | }, 44 | }, 45 | passProps: { 46 | provider, 47 | }, 48 | }, 49 | }), 50 | LogFileViewerScreenPush: (componentId: string) => 51 | Navigation.push(componentId, { 52 | component: { 53 | name: LOG_FILE_VIEWER_SCREEN, 54 | options: { 55 | topBar: { 56 | title: { 57 | text: 'Log Viewer', 58 | }, 59 | rightButtons: [ 60 | { 61 | id: 'clear_log', 62 | text: 'Clear', 63 | color: 'red', 64 | }, 65 | ], 66 | }, 67 | }, 68 | }, 69 | }), 70 | SettingsScreenModal: () => 71 | Navigation.showModal({ 72 | stack: { 73 | children: [ 74 | { 75 | component: { 76 | name: SETTINGS_SCREEN, 77 | options: { 78 | modalPresentationStyle: OptionsModalPresentationStyle.fullScreen, 79 | statusBar: { 80 | style: 'dark', 81 | backgroundColor: 'red', 82 | }, 83 | topBar: { 84 | title: { 85 | text: 'Settings', 86 | }, 87 | rightButtons: [ 88 | { 89 | id: 'done_button', 90 | text: 'Done', 91 | }, 92 | ], 93 | }, 94 | }, 95 | }, 96 | }, 97 | ], 98 | }, 99 | }), 100 | SSHTerminalScreenModal: (name: string, ipv4Address: string) => 101 | Navigation.showModal({ 102 | stack: { 103 | children: [ 104 | { 105 | component: { 106 | name: SSH_TERMINAL_SCREEN, 107 | options: { 108 | topBar: { 109 | title: { 110 | text: 'Terminal', 111 | }, 112 | rightButtons: [ 113 | { 114 | id: 'cancel', 115 | text: 'Cancel', 116 | }, 117 | ], 118 | }, 119 | }, 120 | passProps: { 121 | name, 122 | ipv4Address, 123 | }, 124 | }, 125 | }, 126 | ], 127 | }, 128 | }), 129 | }; 130 | }; 131 | 132 | export default useScreen; 133 | -------------------------------------------------------------------------------- /src/screens/ProviderRegisterScreen/digitalocean_login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import DOCallbackHtml from '../../providers/DigitalOcean/do-api-callback-html'; 3 | import StaticServer from '../../static_server'; 4 | // @ts-ignore 5 | import SafariView from 'react-native-safari-view'; 6 | import { Button, colors, Divider, Input } from 'react-native-elements'; 7 | import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; 8 | import useScreen from '../useScreen'; 9 | import { BACKGROUND_PRIMARY, BACKGROUND_SECONDARY, COLOR_SECONDARY } from '../../theme'; 10 | 11 | const DigitalOceanLogin = (props: any) => { 12 | const [token, setToken] = useState(''); 13 | const { ProviderRegisterScreenPush } = useScreen(); 14 | 15 | const signIn = async () => { 16 | const html = DOCallbackHtml(); 17 | const url = await StaticServer.serveHtml(html); 18 | 19 | SafariView.show({ 20 | url: 21 | 'https://cloud.digitalocean.com/v1/oauth/authorize?response_type=token' + 22 | '&client_id=8d60106cd9109861ce841d4d8cfcc3477a10757f2919601a36873d25be226904' + 23 | `&redirect_uri=${url}` + 24 | '&scope=read%20write', 25 | fromBottom: true, 26 | }); 27 | }; 28 | 29 | const signInByToken = async () => { 30 | const html = DOCallbackHtml(); 31 | const url = await StaticServer.serveHtml(html); 32 | 33 | if (token.length === 0) { 34 | return; 35 | } 36 | 37 | SafariView.show({ 38 | url: url + `#access_token=${token}`, 39 | fromBottom: true, 40 | }); 41 | }; 42 | 43 | const toggleTokenInput = () => { 44 | ProviderRegisterScreenPush({ ...props, tokenInput: true }); 45 | }; 46 | 47 | if (props.tokenInput) { 48 | return ( 49 | 50 | 51 | Connect a DigitalOcean Account 52 | 53 | 54 | 55 | 56 | Provide your personal DigitalOcean API access token to start deploying VPN servers. 57 | 58 | 59 | setToken(value)} 66 | /> 67 | 68 |