├── .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 | 
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 |
78 |
79 | );
80 | }
81 |
82 | return (
83 |
84 |
85 | Connect a DigitalOcean Account
86 |
87 |
88 |
89 |
90 | ZudVPN uses DigitalOcean API to create a VPN server. Choose a step to sign in to DigitalOcean
91 | account. After the sign-in, you will be provided a list of regions to host your VPN server.
92 |
93 |
94 |
95 |
96 |
106 |
107 |
108 | toggleTokenInput()}>
109 | Do you have a personal access token?
110 |
111 | Tap here to
112 | sign in using a token.
113 |
114 |
115 |
116 |
117 |
118 | );
119 | };
120 |
121 | export default DigitalOceanLogin;
122 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=`expr $i + 1`
158 | done
159 | case $i in
160 | 0) set -- ;;
161 | 1) set -- "$args0" ;;
162 | 2) set -- "$args0" "$args1" ;;
163 | 3) set -- "$args0" "$args1" "$args2" ;;
164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=`save "$@"`
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | exec "$JAVACMD" "$@"
184 |
--------------------------------------------------------------------------------
/src/screens/SettingsScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react';
2 | import { Alert, Text, SafeAreaView, ScrollView, FlatList, RefreshControl, ActivityIndicator } from 'react-native';
3 | import { Navigation } from 'react-native-navigation';
4 | // @ts-ignore
5 | import RNNetworkExtension from 'react-native-network-extension';
6 | import { useStore } from '../../store/store';
7 | import ServerItem from './server_item';
8 | import useScreen from '../useScreen';
9 | import withClient from '../../providers/with_client';
10 | import { ProviderListItem } from './provider_list_item';
11 | import { AVAILABLE_PROVIDERS } from '../../providers';
12 | import { Divider, ListItem } from 'react-native-elements';
13 | import { BACKGROUND_PRIMARY, BACKGROUND_SECONDARY, COLOR_SECONDARY } from '../../theme';
14 | import style from './styles';
15 | // @ts-ignore
16 | import SafariView from 'react-native-safari-view';
17 | import { Server } from 'providers/types/Server';
18 | import Client from 'providers/client';
19 |
20 | interface Props {
21 | componentId: string;
22 | client: Client;
23 | }
24 |
25 | const SettingsScreen = (props: Props) => {
26 | const [servers, setServers] = useState(null);
27 | const [refreshing, setRefreshing] = useState(false);
28 | const [{ currentServer, vpnStatus }, { setCurrentVPNServer, setVPNStatus, notify }] = useStore();
29 | const { LogFileViewerScreenPush } = useScreen();
30 |
31 | Navigation.events().registerNavigationButtonPressedListener(({ buttonId, componentId }) => {
32 | if (componentId === props.componentId && buttonId === 'done_button') {
33 | Navigation.dismissModal(props.componentId);
34 | }
35 | });
36 |
37 | const select = (server: Server) => () => {
38 | setVPNStatus('Connecting');
39 |
40 | props.client
41 | .configureServer(server.provider.id, server, notify)
42 | .then(() => {
43 | setCurrentVPNServer(server);
44 | notify('success', 'VPN server is ready for connection');
45 | })
46 | .catch((e: Error) => {
47 | setVPNStatus('Connect');
48 | notify('error', `Failed to connect to VPN server: ${e.message || JSON.stringify(e)}`);
49 | });
50 |
51 | Navigation.dismissModal(props.componentId);
52 | };
53 |
54 | const destroyConfirmed = (uid: string) => {
55 | if (!servers) {
56 | return;
57 | }
58 |
59 | const server = servers.filter((_server) => _server.uid === uid);
60 |
61 | if (!server) {
62 | return;
63 | }
64 |
65 | Promise.all([
66 | props.client.deleteServer(server[0]).catch((e: Error) => e),
67 | RNNetworkExtension.remove().catch((e: Error) => e),
68 | ]);
69 |
70 | if (currentServer !== null && currentServer.uid === uid) {
71 | setCurrentVPNServer(null);
72 | setVPNStatus('Connect');
73 | }
74 |
75 | // remove deleted server from servers list
76 | setServers(servers.filter((_server) => _server.uid !== uid));
77 | };
78 |
79 | const destroy = (uid: string) => () => {
80 | Alert.alert('Warning!', 'Are you sure you want to destroy this server? This action cannot be undone.', [
81 | {
82 | text: 'Destroy',
83 | onPress: () => destroyConfirmed(uid),
84 | style: 'destructive',
85 | },
86 | {
87 | text: 'Cancel',
88 | style: 'cancel',
89 | },
90 | ]);
91 | };
92 |
93 | const retrieveServers = async () => {
94 | const _servers = await props.client.getServers();
95 |
96 | setServers(_servers);
97 | };
98 |
99 | const onRefresh = useCallback(() => {
100 | setRefreshing(true);
101 |
102 | retrieveServers().then(() => setRefreshing(false));
103 | // eslint-disable-next-line react-hooks/exhaustive-deps
104 | }, [refreshing]);
105 |
106 | if (servers === null) {
107 | setTimeout(() => {
108 | retrieveServers();
109 | }, 0);
110 | }
111 |
112 | const togglePiHoleWebPage = () => {
113 | SafariView.show({
114 | url: 'http://pi.hole:81/',
115 | fromBottom: true,
116 | });
117 | };
118 |
119 | return (
120 |
121 | }>
124 | {servers === null && }
125 | {servers !== null &&
126 | servers.length > 0 &&
127 | servers.map((server) => (
128 |
129 | ))}
130 | CLOUD PROVIDERS
131 |
132 | }
135 | keyExtractor={(item, index) => index.toString()}
136 | />
137 |
138 | ADVANCED
139 |
140 | LogFileViewerScreenPush(props.componentId)}>
144 |
145 | {'Application logs'}
146 |
147 |
148 |
149 | {vpnStatus === 'Connected' && (
150 | togglePiHoleWebPage()}>
154 |
155 | {'PiHole Ad-blocker'}
156 |
157 |
158 |
159 | )}
160 |
161 |
162 | );
163 | };
164 |
165 | export default withClient(SettingsScreen);
166 |
--------------------------------------------------------------------------------
/src/providers/DigitalOcean/Deploy.tsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import SSHClient from './../../ssh/client';
4 | import CloudConfig from './cloudconfig';
5 | import { sleep } from '../../helper';
6 | import Keygen, { Keypair } from './../../ssh/keygen';
7 | import logger from '../../logger';
8 | import Keychain from '../../keychain';
9 | import 'react-native-get-random-values';
10 | // @ts-ignore
11 | import { v4 as uuidv4 } from 'uuid';
12 | import ApiClient from 'providers/DigitalOcean/ApiClient';
13 | import { Notify } from 'store/types/Notify';
14 | import { VPNCredentials } from 'providers/types/VPNCredentials';
15 | import { Server } from 'providers/types/Server';
16 |
17 | const DROPLET_BASE_NAME = 'zudvpn';
18 | const DROPLET_IMAGE = 'rancheros';
19 | const DROPLET_SIZE = 's-1vcpu-1gb';
20 |
21 | class Deploy {
22 | client: ApiClient;
23 | notify: Notify;
24 |
25 | constructor(client: ApiClient, notify: Notify) {
26 | this.client = client;
27 | this.notify = notify;
28 | }
29 |
30 | async run(region: string): Promise {
31 | this.notify('progress', 'Creating a server');
32 | const name = this.generateName(region);
33 | const sshKeyPair = await this.getSSHKeyPair(name);
34 | let userData = CloudConfig(sshKeyPair.authorizedKey);
35 |
36 | logger.debug(['[DigitalOcean] Generated user data: ', userData]);
37 | let droplet = await this.client.createDroplet(
38 | sshKeyPair.fingerprint,
39 | name,
40 | region,
41 | DROPLET_SIZE,
42 | userData,
43 | DROPLET_IMAGE,
44 | DROPLET_BASE_NAME,
45 | );
46 |
47 | await this.addFirewallToDroplet(DROPLET_BASE_NAME, droplet.id);
48 |
49 | let ipAddress = await this.getIpAddress(droplet);
50 |
51 | const server = {
52 | uid: droplet.id,
53 | provider: {
54 | id: 'digitalocean',
55 | name: 'DigitalOcean',
56 | },
57 | name: droplet.name,
58 | region: {
59 | name: droplet.region.name,
60 | slug: droplet.region.slug,
61 | available: droplet.region.available,
62 | },
63 | ipv4Address: ipAddress,
64 | };
65 |
66 | return await this.read(server, sshKeyPair);
67 | }
68 |
69 | async read(server: Server, sshKeyPair: Keypair): Promise {
70 | let sshClient = new SSHClient(sshKeyPair, 'rancher', server.ipv4Address, 22);
71 | await this.waitForSSHConnection(sshClient);
72 |
73 | await this.waitForVPNService(sshClient);
74 |
75 | this.notify('progress', 'Loading VPN authentication credentials');
76 | let [domain, password] = await Promise.all([
77 | sshClient.run('cat /home/rancher/domain'),
78 | sshClient.run('docker exec strongswan /bin/sh -c "cat /etc/ipsec.d/client.password"'),
79 | ]);
80 |
81 | domain = domain.replace(/[\n\r]+/g, '');
82 | password = password.replace(/[\n\r]+/g, '');
83 |
84 | sshClient.closeSession();
85 |
86 | return {
87 | server,
88 | ipAddress: server.ipv4Address,
89 | domain,
90 | username: 'vpn',
91 | password,
92 | };
93 | }
94 |
95 | private async getSSHKeyPair(name: string): Promise {
96 | const sshKeyPair = await Keygen.generateKeyPair();
97 | logger.debug(['[DigitalOcean] SSH Keypair:', sshKeyPair]);
98 |
99 | await this.client.createSSHKey(name, sshKeyPair.authorizedKey);
100 |
101 | // Save SSH by droplet key (used to ssh terminal connect)
102 | Keychain.setSSHKeyPair(name, sshKeyPair);
103 |
104 | return sshKeyPair;
105 | }
106 |
107 | private async addFirewallToDroplet(droplet_name: string, dropletId: string): Promise {
108 | this.notify('progress', 'Creating a firewall');
109 |
110 | const firewallName = droplet_name + '-firewall';
111 |
112 | const firewalls = await this.client.getAllFirewalls();
113 |
114 | if (firewalls && firewalls.length > 0) {
115 | const firewall = firewalls.find((f: any) => f.name === firewallName);
116 |
117 | if (firewall) {
118 | await this.client.addDropletToFirewall(firewall.id, dropletId);
119 | }
120 | } else {
121 | await this.client.createFirewall(firewallName, dropletId);
122 | }
123 | }
124 |
125 | private async waitForVPNService(sshClient: SSHClient): Promise {
126 | this.notify('progress', 'Waiting for VPN service');
127 |
128 | let countWaitingForVPN = 10;
129 | do {
130 | countWaitingForVPN--;
131 | try {
132 | await sshClient.run('docker logs strongswan --until=5s &>/dev/null');
133 | countWaitingForVPN = 0;
134 | } catch (e) {
135 | logger.debug(`Attempt ${10 - countWaitingForVPN}/10 failed, reason: ${JSON.stringify(e)}`);
136 |
137 | if (countWaitingForVPN === 0) {
138 | throw e;
139 | }
140 | }
141 | await sleep(countWaitingForVPN * 1000);
142 | } while (countWaitingForVPN > 0);
143 | }
144 |
145 | private async waitForSSHConnection(sshClient: SSHClient): Promise {
146 | this.notify('progress', 'Connecting to server');
147 |
148 | let trialLeft = 10;
149 | do {
150 | trialLeft--;
151 | try {
152 | await sshClient.openSession();
153 | trialLeft = 0;
154 | } catch (e) {
155 | logger.debug(
156 | `[DigitalOcean] SSH connection not ready: ${JSON.stringify(e)}, retrying ${10 - trialLeft}/10`,
157 | );
158 | }
159 | await sleep(trialLeft * 1000);
160 | } while (trialLeft > 0);
161 | }
162 |
163 | private async getIpAddress(droplet: any): Promise {
164 | this.notify('progress', 'Waiting for server IP address');
165 |
166 | for (let i = 1; i < 10; i++) {
167 | for (const ip of droplet.networks.v4) {
168 | if (ip.type === 'public' && ip.ip_address) {
169 | this.notify('progress', 'VPN IP address: ' + ip.ip_address);
170 |
171 | return ip.ip_address;
172 | }
173 | }
174 |
175 | await sleep(i * 1000);
176 |
177 | droplet = await this.client.getDropletById(droplet.id);
178 | }
179 |
180 | throw new Error('Timed out waiting for server provisioning.');
181 | }
182 |
183 | private generateName = (region: string) => `${DROPLET_BASE_NAME}-${region}-${uuidv4().slice(-4)}`;
184 | }
185 |
186 | export default Deploy;
187 |
--------------------------------------------------------------------------------
/src/providers/DigitalOcean/ApiClient.tsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import ProviderAuthenticationError from '../../exceptions/ProviderAuthenticationError';
4 | import ProviderInvalidContentError from '../../exceptions/ProviderInvalidContentError';
5 | import ProviderUnexpectedError from '../../exceptions/ProviderUnexpectedError';
6 | import { Account } from 'providers/types/Account';
7 |
8 | class ApiClient {
9 | token: string;
10 |
11 | constructor(token: string) {
12 | this.token = token;
13 | }
14 |
15 | async makeRequest(method: string, url: string, body: null | string = null) {
16 | let response = await fetch(url, {
17 | method,
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | Authorization: 'Bearer ' + this.token,
21 | },
22 | body,
23 | });
24 |
25 | console.log(`[DigitalOcean API] url: ${url}, response: `, response);
26 |
27 | if (!response.hasOwnProperty('headers')) {
28 | throw new ProviderInvalidContentError('DigitalOcean API returned an invalid response.');
29 | }
30 |
31 | let content: any = await response.text();
32 | console.log('[DigitalOcean API] content: ', content);
33 | if (response.headers.get('content-type')?.includes('application/json')) {
34 | content = JSON.parse(content);
35 | }
36 |
37 | if (response.status > 399) {
38 | const message = content.hasOwnProperty('message') ? content.message : JSON.stringify(content);
39 |
40 | if (response.status === 401) {
41 | throw new ProviderAuthenticationError(message);
42 | }
43 |
44 | throw new ProviderUnexpectedError(message);
45 | }
46 |
47 | return content;
48 | }
49 |
50 | async getAccount(): Promise {
51 | let response = await this.makeRequest('GET', 'https://api.digitalocean.com/v2/account');
52 |
53 | return { email: response.account.email };
54 | }
55 |
56 | async createSSHKey(name: string, public_key: string): Promise {
57 | let response = await this.makeRequest(
58 | 'POST',
59 | 'https://api.digitalocean.com/v2/account/keys',
60 | JSON.stringify({
61 | name,
62 | public_key,
63 | }),
64 | );
65 |
66 | return response.ssh_key.id;
67 | }
68 |
69 | async getDropletsByTag(tag: string) {
70 | let response = await this.makeRequest('GET', 'https://api.digitalocean.com/v2/droplets?tag_name=' + tag);
71 |
72 | return response.droplets;
73 | }
74 |
75 | async getDropletById(dropletId: string) {
76 | let response = await this.makeRequest('GET', 'https://api.digitalocean.com/v2/droplets/' + dropletId);
77 |
78 | return response.droplet;
79 | }
80 |
81 | async createDroplet(
82 | ssh_fingerprint: string,
83 | name: string,
84 | region: string,
85 | size: string,
86 | user_data: string,
87 | image: string,
88 | tag: string,
89 | ) {
90 | let response = await this.makeRequest(
91 | 'POST',
92 | 'https://api.digitalocean.com/v2/droplets',
93 | JSON.stringify({
94 | name,
95 | region,
96 | size,
97 | image,
98 | ssh_keys: [ssh_fingerprint],
99 | user_data,
100 | tags: [tag],
101 | backups: false,
102 | private_networking: false,
103 | monitoring: false,
104 | ipv6: true,
105 | volumes: null,
106 | }),
107 | );
108 |
109 | return response.droplet;
110 | }
111 |
112 | async deleteDroplet(dropletId: string): Promise {
113 | await this.makeRequest('DELETE', `https://api.digitalocean.com/v2/droplets/${dropletId}`);
114 | }
115 |
116 | async deleteSshKey(fingerprint: string): Promise {
117 | await this.makeRequest('DELETE', `https://api.digitalocean.com/v2/account/keys/${fingerprint}`);
118 | }
119 |
120 | async getRegions() {
121 | let response = await this.makeRequest('GET', 'https://api.digitalocean.com/v2/regions');
122 |
123 | return response.regions.sort((a: any, b: any) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0));
124 | }
125 |
126 | async getAllFirewalls() {
127 | let response = await this.makeRequest('GET', 'https://api.digitalocean.com/v2/firewalls');
128 |
129 | return response.firewalls;
130 | }
131 |
132 | async addDropletToFirewall(firewallId: string, dropletId: string) {
133 | await this.makeRequest(
134 | 'POST',
135 | `https://api.digitalocean.com/v2/firewalls/${firewallId}/droplets`,
136 | JSON.stringify({
137 | droplet_ids: [dropletId],
138 | }),
139 | );
140 | }
141 |
142 | async createFirewall(name: string, dropletId: string) {
143 | let response = await this.makeRequest(
144 | 'POST',
145 | 'https://api.digitalocean.com/v2/firewalls',
146 | JSON.stringify({
147 | name,
148 | inbound_rules: [
149 | {
150 | protocol: 'icmp',
151 | sources: {
152 | addresses: ['0.0.0.0/0', '::/0'],
153 | },
154 | },
155 | {
156 | protocol: 'tcp',
157 | ports: '80',
158 | sources: {
159 | addresses: ['0.0.0.0/0', '::/0'],
160 | },
161 | },
162 | {
163 | protocol: 'tcp',
164 | ports: '443',
165 | sources: {
166 | addresses: ['0.0.0.0/0', '::/0'],
167 | },
168 | },
169 | {
170 | protocol: 'tcp',
171 | ports: '22',
172 | sources: {
173 | addresses: ['0.0.0.0/0', '::/0'],
174 | },
175 | },
176 | {
177 | protocol: 'udp',
178 | ports: '500',
179 | sources: {
180 | addresses: ['0.0.0.0/0', '::/0'],
181 | },
182 | },
183 | {
184 | protocol: 'udp',
185 | ports: '4500',
186 | sources: {
187 | addresses: ['0.0.0.0/0', '::/0'],
188 | },
189 | },
190 | ],
191 | outbound_rules: [
192 | {
193 | protocol: 'icmp',
194 | destinations: {
195 | addresses: ['0.0.0.0/0', '::/0'],
196 | },
197 | },
198 | {
199 | protocol: 'tcp',
200 | ports: 'all',
201 | destinations: {
202 | addresses: ['0.0.0.0/0', '::/0'],
203 | },
204 | },
205 | {
206 | protocol: 'udp',
207 | ports: 'all',
208 | destinations: {
209 | addresses: ['0.0.0.0/0', '::/0'],
210 | },
211 | },
212 | ],
213 | droplet_ids: [dropletId],
214 | }),
215 | );
216 |
217 | return response.firewall.id;
218 | }
219 | }
220 |
221 | export default ApiClient;
222 |
--------------------------------------------------------------------------------
/src/providers/DigitalOcean/cloudconfig.tsx:
--------------------------------------------------------------------------------
1 | export default (key: string): string => {
2 | return `#cloud-config
3 | runcmd:
4 | - sudo bash -c 'echo "1.1.1.2 pi.hole" >> /etc/hosts'
5 | write_files:
6 | - container: network
7 | path: /var/lib/iptables/rules-save
8 | permission: "0755"
9 | owner: root:root
10 | content: |
11 | *mangle
12 | :PREROUTING ACCEPT [0:0]
13 | :INPUT ACCEPT [0:0]
14 | :FORWARD ACCEPT [0:0]
15 | :OUTPUT ACCEPT [0:0]
16 | :POSTROUTING ACCEPT [0:0]
17 | -A FORWARD -s 192.168.99.0/24 -o eth0 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
18 | COMMIT
19 | *nat
20 | :PREROUTING ACCEPT [0:0]
21 | :POSTROUTING ACCEPT [0:0]
22 | -A POSTROUTING -s 192.168.99.0/24 -m policy --pol none --dir out -j MASQUERADE
23 | COMMIT
24 | *filter
25 | :INPUT DROP [0:0]
26 | :FORWARD DROP [0:0]
27 | :OUTPUT ACCEPT [0:0]
28 | -A INPUT -i lo -j ACCEPT
29 | -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
30 | -A INPUT -p esp -j ACCEPT
31 | -A INPUT -p ah -j ACCEPT
32 | -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT
33 | -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT
34 | -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
35 | -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 20 --rttl --name SSH -j DROP
36 | -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
37 | -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT
38 | -A INPUT -p tcp --destination-port 443 -j REJECT --reject-with tcp-reset
39 | -A INPUT -p udp --destination-port 80 -j REJECT --reject-with icmp-port-unreachable
40 | -A INPUT -p udp --destination-port 443 -j REJECT --reject-with icmp-port-unreachable
41 | -A INPUT -d 1.1.1.2 -p udp -j ACCEPT
42 | -A INPUT -d 1.1.1.2 -p tcp -j ACCEPT
43 | -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
44 | -A FORWARD -m conntrack --ctstate NEW -s 192.168.99.0/24 -m policy --pol ipsec --dir in -j ACCEPT
45 | COMMIT
46 | - container: network
47 | path: /var/lib/ip6tables/rules-save
48 | permission: "0755"
49 | owner: root:root
50 | content: |
51 | *nat
52 | :PREROUTING ACCEPT [0:0]
53 | :POSTROUTING ACCEPT [0:0]
54 | -A POSTROUTING -s fd9d:bc11:4020::/48 -m policy --pol none --dir out -j MASQUERADE
55 | COMMIT
56 | *filter
57 | :INPUT DROP [0:0]
58 | :FORWARD DROP [0:0]
59 | :OUTPUT ACCEPT [0:0]
60 | :ICMPV6-CHECK - [0:0]
61 | :ICMPV6-CHECK-LOG - [0:0]
62 | -A INPUT -i lo -j ACCEPT
63 | -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
64 | -A INPUT -p esp -j ACCEPT
65 | -A INPUT -m ah -j ACCEPT
66 | -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT
67 | -A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT
68 | -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT
69 | -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
70 | -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT
71 | -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
72 | -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 20 --rttl --name SSH -j DROP
73 | -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
74 | -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT
75 | -A INPUT -p tcp --destination-port 443 -j REJECT --reject-with tcp-reset
76 | -A INPUT -p udp --destination-port 80 -j REJECT --reject-with icmp6-port-unreachable
77 | -A INPUT -p udp --destination-port 443 -j REJECT --reject-with icmp6-port-unreachable
78 | -A INPUT -d fd9d:bc11:4020::/48 -p udp -j ACCEPT
79 | -A INPUT -d fd9d:bc11:4020::/48 -p tcp -j ACCEPT
80 | -A FORWARD -j ICMPV6-CHECK
81 | -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
82 | -A FORWARD -m conntrack --ctstate NEW -s fd9d:bc11:4020::/48 -m policy --pol ipsec --dir in -j ACCEPT
83 | -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG
84 | -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-advertisement -j ICMPV6-CHECK-LOG
85 | -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-solicitation -j ICMPV6-CHECK-LOG
86 | -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-advertisement -j ICMPV6-CHECK-LOG
87 | -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP "
88 | -A ICMPV6-CHECK-LOG -j DROP
89 | COMMIT
90 | - path: /etc/rc.local
91 | permissions: "0755"
92 | owner: root:root
93 | content: |
94 | #!/bin/bash
95 |
96 | # Configuration for SSL Certificates with Caddy
97 | mkdir -p /home/rancher/.caddy
98 | echo "$(wget -qO- http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)" > /home/rancher/ipv4_address
99 | echo "$(cat /home/rancher/ipv4_address | tr "." "-").zudvpn.com" > /home/rancher/domain
100 | cat /home/rancher/domain > /home/rancher/Caddyfile
101 | echo "tls rancher@$(cat /home/rancher/domain)" >> /home/rancher/Caddyfile
102 | mkdir -p /home/rancher/ipsec.d/certs /home/rancher/ipsec.d/private /home/rancher/ipsec.d/cacerts
103 |
104 | wait-for-docker
105 |
106 | system-docker cp /sbin/iptables-restore network:/sbin/
107 | system-docker cp /sbin/ip6tables-restore network:/sbin/
108 | system-docker exec -d network bash -c "iptables-restore --noflush < /var/lib/iptables/rules-save"
109 | system-docker exec -d network bash -c "ip6tables-restore --noflush < /var/lib/ip6tables/rules-save"
110 |
111 | echo 'docker run --rm radial/busyboxplus:curl curl $@' > /home/rancher/curl && chmod +x /home/rancher/curl
112 | /home/rancher/curl -Ss https://letsencrypt.org/certs/letsencryptauthorityx3.pem.txt > /home/rancher/ipsec.d/certs/chain.pem
113 |
114 | docker pull abiosoft/caddy:latest
115 | docker run --rm --name caddy -d -v /home/rancher/Caddyfile:/etc/Caddyfile -v /home/rancher/.caddy:/root/.caddy -p 80:80 -p 443:443 -e ACME_AGREE=true abiosoft/caddy
116 |
117 | sleep 30
118 | cp $(find /home/rancher/.caddy -type f -iname "*zudvpn.com.key") /home/rancher/ipsec.d/private/privkey.pem
119 | cp $(find /home/rancher/.caddy -type f -iname "*zudvpn.com.crt") /home/rancher/ipsec.d/certs/cert.pem
120 | cat /home/rancher/ipsec.d/certs/cert.pem /home/rancher/ipsec.d/certs/chain.pem > /home/rancher/ipsec.d/certs/fullchain.pem
121 | docker stop caddy
122 |
123 | docker pull zudvpn/strongswan:0.1.8
124 | docker run --name strongswan -d -e VPN_DNS="1.1.1.2" -e DUMMY_DEVICE="1.1.1.2/32" -e VPN_DOMAIN=$(cat /home/rancher/domain) --privileged --net=host -v /home/rancher/ipsec.d:/etc/ipsec.d -v strongswan.d:/etc/strongswan.d -v /lib/modules:/lib/modules -v /etc/localtime:/etc/localtime zudvpn/strongswan:0.1.8
125 |
126 | docker pull pihole/pihole:latest
127 | docker run --name pihole -d --net=host -e DNS1=1.1.1.1 -e ServerIP=1.1.1.2 -e ServerIPv6=fd9d:bc11:4020:: -e WEBPASSWORD=zudvpn -e WEB_PORT=81 -v pihole-etc:/etc/pihole -v pihole-dnsmasq.d:/etc/dnsmasq.d pihole/pihole:latest
128 | ssh_authorized_keys:
129 | - ${key}
130 | rancher:
131 | #disable:
132 | # - password
133 | # - autologin
134 | ssh:
135 | port: 22
136 | network:
137 | interfaces:
138 | eth0:
139 | pre_up:
140 | - modprobe dummy
141 | - ip link add dummy0 type dummy
142 | - ip addr add 1.1.1.2/32 dev dummy0
143 | - ip addr add fd9d:bc11:4020::/48 dev dummy0
144 | - ip link set dummy0 up
145 | sysctl:
146 | net.ipv4.ip_forward: 1
147 | net.ipv4.conf.all.forwarding: 1
148 | net.ipv6.conf.all.forwarding: 1
149 | net.ipv4.conf.all.accept_source_route: 0
150 | net.ipv4.conf.default.accept_source_route: 0
151 | net.ipv4.conf.all.accept_redirects: 0
152 | net.ipv4.conf.default.accept_redirects: 0
153 | net.ipv4.conf.all.secure_redirects: 0
154 | net.ipv4.conf.default.secure_redirects: 0
155 | net.ipv4.icmp_ignore_bogus_error_responses: 1
156 | net.ipv4.conf.all.rp_filter: 1
157 | net.ipv4.conf.default.rp_filter: 1
158 | net.ipv4.conf.all.send_redirects: 0
159 | net.ipv4.conf.all.send_redirects: 0
160 | `;
161 | };
162 |
--------------------------------------------------------------------------------
/assets/terminal/xterm-addon-fit.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack://FitAddon/webpack/universalModuleDefinition","webpack://FitAddon/webpack/bootstrap","webpack://FitAddon/./src/FitAddon.ts"],"names":["root","factory","exports","module","define","amd","window","installedModules","__webpack_require__","moduleId","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","activate","terminal","this","_terminal","dispose","fit","dims","proposeDimensions","core","_core","rows","cols","_renderService","clear","resize","element","parentElement","parentElementStyle","getComputedStyle","parentElementHeight","parseInt","getPropertyValue","parentElementWidth","Math","max","elementStyle","availableHeight","availableWidth","viewport","scrollBarWidth","floor","dimensions","actualCellWidth","actualCellHeight","FitAddon"],"mappings":"CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAkB,SAAID,IAEtBD,EAAe,SAAIC,IARrB,CASGK,QAAQ,WACX,O,YCTE,IAAIC,EAAmB,GAGvB,SAASC,EAAoBC,GAG5B,GAAGF,EAAiBE,GACnB,OAAOF,EAAiBE,GAAUP,QAGnC,IAAIC,EAASI,EAAiBE,GAAY,CACzCC,EAAGD,EACHE,GAAG,EACHT,QAAS,IAUV,OANAU,EAAQH,GAAUI,KAAKV,EAAOD,QAASC,EAAQA,EAAOD,QAASM,GAG/DL,EAAOQ,GAAI,EAGJR,EAAOD,QA0Df,OArDAM,EAAoBM,EAAIF,EAGxBJ,EAAoBO,EAAIR,EAGxBC,EAAoBQ,EAAI,SAASd,EAASe,EAAMC,GAC3CV,EAAoBW,EAAEjB,EAASe,IAClCG,OAAOC,eAAenB,EAASe,EAAM,CAAEK,YAAY,EAAMC,IAAKL,KAKhEV,EAAoBgB,EAAI,SAAStB,GACX,oBAAXuB,QAA0BA,OAAOC,aAC1CN,OAAOC,eAAenB,EAASuB,OAAOC,YAAa,CAAEC,MAAO,WAE7DP,OAAOC,eAAenB,EAAS,aAAc,CAAEyB,OAAO,KAQvDnB,EAAoBoB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQnB,EAAoBmB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKX,OAAOY,OAAO,MAGvB,GAFAxB,EAAoBgB,EAAEO,GACtBX,OAAOC,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOnB,EAAoBQ,EAAEe,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRvB,EAAoB2B,EAAI,SAAShC,GAChC,IAAIe,EAASf,GAAUA,EAAO2B,WAC7B,WAAwB,OAAO3B,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAK,EAAoBQ,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRV,EAAoBW,EAAI,SAASiB,EAAQC,GAAY,OAAOjB,OAAOkB,UAAUC,eAAe1B,KAAKuB,EAAQC,IAGzG7B,EAAoBgC,EAAI,GAIjBhC,EAAoBA,EAAoBiC,EAAI,G,kGC/DrD,IAGA,aAGE,cAwDF,OAtDS,YAAAC,SAAP,SAAgBC,GACdC,KAAKC,UAAYF,GAGZ,YAAAG,QAAP,aAEO,YAAAC,IAAP,WACE,IAAMC,EAAOJ,KAAKK,oBAClB,GAAKD,GAASJ,KAAKC,UAAnB,CAKA,IAAMK,EAAQN,KAAKC,UAAkBM,MAGjCP,KAAKC,UAAUO,OAASJ,EAAKI,MAAQR,KAAKC,UAAUQ,OAASL,EAAKK,OACpEH,EAAKI,eAAeC,QACpBX,KAAKC,UAAUW,OAAOR,EAAKK,KAAML,EAAKI,SAInC,YAAAH,kBAAP,WACE,GAAKL,KAAKC,WAILD,KAAKC,UAAUY,SAAYb,KAAKC,UAAUY,QAAQC,cAAvD,CAKA,IAAMR,EAAQN,KAAKC,UAAkBM,MAE/BQ,EAAqBrD,OAAOsD,iBAAiBhB,KAAKC,UAAUY,QAAQC,eACpEG,EAAsBC,SAASH,EAAmBI,iBAAiB,WACnEC,EAAqBC,KAAKC,IAAI,EAAGJ,SAASH,EAAmBI,iBAAiB,WAC9EI,EAAe7D,OAAOsD,iBAAiBhB,KAAKC,UAAUY,SAStDW,EAAkBP,GAPjBC,SAASK,EAAaJ,iBAAiB,gBACpCD,SAASK,EAAaJ,iBAAiB,oBAO3CM,EAAiBL,GANdF,SAASK,EAAaJ,iBAAiB,kBACxCD,SAASK,EAAaJ,iBAAiB,kBAKiBb,EAAKoB,SAASC,eAK9E,MAJiB,CACflB,KAAMY,KAAKC,IAzDI,EAyDcD,KAAKO,MAAMH,EAAiBnB,EAAKI,eAAemB,WAAWC,kBACxFtB,KAAMa,KAAKC,IAzDI,EAyDcD,KAAKO,MAAMJ,EAAkBlB,EAAKI,eAAemB,WAAWE,sBAI/F,EA3DA,GAAa,EAAAC","file":"xterm-addon-fit.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"FitAddon\"] = factory();\n\telse\n\t\troot[\"FitAddon\"] = factory();\n})(window, function() {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal, ITerminalAddon } from 'xterm';\n\ninterface ITerminalDimensions {\n /**\n * The number of rows in the terminal.\n */\n rows: number;\n\n /**\n * The number of columns in the terminal.\n */\n cols: number;\n}\n\nconst MINIMUM_COLS = 2;\nconst MINIMUM_ROWS = 1;\n\nexport class FitAddon implements ITerminalAddon {\n private _terminal: Terminal | undefined;\n\n constructor() {}\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n }\n\n public dispose(): void {}\n\n public fit(): void {\n const dims = this.proposeDimensions();\n if (!dims || !this._terminal) {\n return;\n }\n\n // TODO: Remove reliance on private API\n const core = (this._terminal as any)._core;\n\n // Force a full render\n if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {\n core._renderService.clear();\n this._terminal.resize(dims.cols, dims.rows);\n }\n }\n\n public proposeDimensions(): ITerminalDimensions | undefined {\n if (!this._terminal) {\n return undefined;\n }\n\n if (!this._terminal.element || !this._terminal.element.parentElement) {\n return undefined;\n }\n\n // TODO: Remove reliance on private API\n const core = (this._terminal as any)._core;\n\n const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);\n const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));\n const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));\n const elementStyle = window.getComputedStyle(this._terminal.element);\n const elementPadding = {\n top: parseInt(elementStyle.getPropertyValue('padding-top')),\n bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),\n right: parseInt(elementStyle.getPropertyValue('padding-right')),\n left: parseInt(elementStyle.getPropertyValue('padding-left'))\n };\n const elementPaddingVer = elementPadding.top + elementPadding.bottom;\n const elementPaddingHor = elementPadding.right + elementPadding.left;\n const availableHeight = parentElementHeight - elementPaddingVer;\n const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;\n const geometry = {\n cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),\n rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))\n };\n return geometry;\n }\n}\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 |
3 | import com.android.build.OutputFile
4 |
5 | /**
6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
7 | * and bundleReleaseJsAndAssets).
8 | * These basically call `react-native bundle` with the correct arguments during the Android build
9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
10 | * bundle directly from the development server. Below you can see all the possible configurations
11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the
12 | * `apply from: "../../node_modules/react-native/react.gradle"` line.
13 | *
14 | * project.ext.react = [
15 | * // the name of the generated asset file containing your JS bundle
16 | * bundleAssetName: "index.android.bundle",
17 | *
18 | * // the entry file for bundle generation. If none specified and
19 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is
20 | * // default. Can be overridden with ENTRY_FILE environment variable.
21 | * entryFile: "index.android.js",
22 | *
23 | * // https://reactnative.dev/docs/performance#enable-the-ram-format
24 | * bundleCommand: "ram-bundle",
25 | *
26 | * // whether to bundle JS and assets in debug mode
27 | * bundleInDebug: false,
28 | *
29 | * // whether to bundle JS and assets in release mode
30 | * bundleInRelease: true,
31 | *
32 | * // whether to bundle JS and assets in another build variant (if configured).
33 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
34 | * // The configuration property can be in the following formats
35 | * // 'bundleIn${productFlavor}${buildType}'
36 | * // 'bundleIn${buildType}'
37 | * // bundleInFreeDebug: true,
38 | * // bundleInPaidRelease: true,
39 | * // bundleInBeta: true,
40 | *
41 | * // whether to disable dev mode in custom build variants (by default only disabled in release)
42 | * // for example: to disable dev mode in the staging build type (if configured)
43 | * devDisabledInStaging: true,
44 | * // The configuration property can be in the following formats
45 | * // 'devDisabledIn${productFlavor}${buildType}'
46 | * // 'devDisabledIn${buildType}'
47 | *
48 | * // the root of your project, i.e. where "package.json" lives
49 | * root: "../../",
50 | *
51 | * // where to put the JS bundle asset in debug mode
52 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
53 | *
54 | * // where to put the JS bundle asset in release mode
55 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
56 | *
57 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
58 | * // require('./image.png')), in debug mode
59 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
60 | *
61 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
62 | * // require('./image.png')), in release mode
63 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
64 | *
65 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
66 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
67 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle
68 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
69 | * // for example, you might want to remove it from here.
70 | * inputExcludes: ["android/**", "ios/**"],
71 | *
72 | * // override which node gets called and with what additional arguments
73 | * nodeExecutableAndArgs: ["node"],
74 | *
75 | * // supply additional arguments to the packager
76 | * extraPackagerArgs: []
77 | * ]
78 | */
79 |
80 | project.ext.react = [
81 | enableHermes: false, // clean and rebuild if changing
82 | ]
83 |
84 | apply from: "../../node_modules/react-native/react.gradle"
85 |
86 | /**
87 | * Set this to true to create two separate APKs instead of one:
88 | * - An APK that only works on ARM devices
89 | * - An APK that only works on x86 devices
90 | * The advantage is the size of the APK is reduced by about 4MB.
91 | * Upload all the APKs to the Play Store and people will download
92 | * the correct one based on the CPU architecture of their device.
93 | */
94 | def enableSeparateBuildPerCPUArchitecture = false
95 |
96 | /**
97 | * Run Proguard to shrink the Java bytecode in release builds.
98 | */
99 | def enableProguardInReleaseBuilds = false
100 |
101 | /**
102 | * The preferred build flavor of JavaScriptCore.
103 | *
104 | * For example, to use the international variant, you can use:
105 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
106 | *
107 | * The international variant includes ICU i18n library and necessary data
108 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
109 | * give correct results when using with locales other than en-US. Note that
110 | * this variant is about 6MiB larger per architecture than default.
111 | */
112 | def jscFlavor = 'org.webkit:android-jsc:+'
113 |
114 | /**
115 | * Whether to enable the Hermes VM.
116 | *
117 | * This should be set on project.ext.react and mirrored here. If it is not set
118 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
119 | * and the benefits of using Hermes will therefore be sharply reduced.
120 | */
121 | def enableHermes = project.ext.react.get("enableHermes", false);
122 |
123 | android {
124 | compileSdkVersion rootProject.ext.compileSdkVersion
125 |
126 | compileOptions {
127 | sourceCompatibility JavaVersion.VERSION_1_8
128 | targetCompatibility JavaVersion.VERSION_1_8
129 | }
130 |
131 | defaultConfig {
132 | applicationId "com.zudvpn"
133 | minSdkVersion rootProject.ext.minSdkVersion
134 | targetSdkVersion rootProject.ext.targetSdkVersion
135 | versionCode 1
136 | versionName "1.0"
137 | }
138 | splits {
139 | abi {
140 | reset()
141 | enable enableSeparateBuildPerCPUArchitecture
142 | universalApk false // If true, also generate a universal APK
143 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
144 | }
145 | }
146 | signingConfigs {
147 | debug {
148 | storeFile file('debug.keystore')
149 | storePassword 'android'
150 | keyAlias 'androiddebugkey'
151 | keyPassword 'android'
152 | }
153 | }
154 | buildTypes {
155 | debug {
156 | signingConfig signingConfigs.debug
157 | }
158 | release {
159 | // Caution! In production, you need to generate your own keystore file.
160 | // see https://reactnative.dev/docs/signed-apk-android.
161 | signingConfig signingConfigs.debug
162 | minifyEnabled enableProguardInReleaseBuilds
163 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
164 | }
165 | }
166 |
167 | // applicationVariants are e.g. debug, release
168 | applicationVariants.all { variant ->
169 | variant.outputs.each { output ->
170 | // For each separate APK per architecture, set a unique version code as described here:
171 | // https://developer.android.com/studio/build/configure-apk-splits.html
172 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
173 | def abi = output.getFilter(OutputFile.ABI)
174 | if (abi != null) { // null for the universal-debug, universal-release variants
175 | output.versionCodeOverride =
176 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
177 | }
178 |
179 | }
180 | }
181 | }
182 |
183 | dependencies {
184 | implementation project(':react-native-vector-icons')
185 | implementation fileTree(dir: "libs", include: ["*.jar"])
186 | //noinspection GradleDynamicVersion
187 | implementation "com.facebook.react:react-native:+" // From node_modules
188 |
189 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
190 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
191 | exclude group:'com.facebook.fbjni'
192 | }
193 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
194 | exclude group:'com.facebook.flipper'
195 | exclude group:'com.squareup.okhttp3', module:'okhttp'
196 | }
197 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
198 | exclude group:'com.facebook.flipper'
199 | }
200 |
201 | if (enableHermes) {
202 | def hermesPath = "../../node_modules/hermes-engine/android/";
203 | debugImplementation files(hermesPath + "hermes-debug.aar")
204 | releaseImplementation files(hermesPath + "hermes-release.aar")
205 | } else {
206 | implementation jscFlavor
207 | }
208 | }
209 |
210 | // Run this once to be able to run the application with BUCK
211 | // puts all compile dependencies into folder libs for BUCK to use
212 | task copyDownloadableDepsToLibs(type: Copy) {
213 | from configurations.compile
214 | into 'libs'
215 | }
216 |
217 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
218 |
--------------------------------------------------------------------------------