react-native-ovpn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/Openvpn.podspec +34 -0
- package/README.md +80 -0
- package/android/build.gradle +98 -0
- package/android/libs/README.md +46 -0
- package/android/libs/ics-openvpn.aar +0 -0
- package/android/src/main/AndroidManifest.xml +54 -0
- package/android/src/main/java/com/openvpn/NotificationHelper.kt +59 -0
- package/android/src/main/java/com/openvpn/OpenvpnEventBus.kt +52 -0
- package/android/src/main/java/com/openvpn/OpenvpnException.kt +6 -0
- package/android/src/main/java/com/openvpn/OpenvpnModule.kt +140 -0
- package/android/src/main/java/com/openvpn/OpenvpnPackage.kt +31 -0
- package/android/src/main/java/com/openvpn/OpenvpnService.kt +248 -0
- package/android/src/main/java/com/openvpn/PermissionLauncher.kt +39 -0
- package/android/src/main/java/com/openvpn/ProfileBuilder.kt +68 -0
- package/android/src/main/res/drawable/ic_vpn_default.xml +10 -0
- package/android/src/main/res/values/strings.xml +6 -0
- package/android/src/test/java/com/openvpn/NotificationHelperTest.kt +49 -0
- package/android/src/test/java/com/openvpn/ProfileBuilderTest.kt +83 -0
- package/app.plugin.js +3 -0
- package/ios/Openvpn-Bridging-Header.h +8 -0
- package/ios/Openvpn.h +5 -0
- package/ios/Openvpn.mm +123 -0
- package/ios/OpenvpnAppGroup.swift +59 -0
- package/ios/OpenvpnConstants.swift +46 -0
- package/ios/OpenvpnEventBridge.swift +58 -0
- package/ios/OpenvpnManager.swift +219 -0
- package/ios/PacketTunnelProvider/Info.plist +31 -0
- package/ios/PacketTunnelProvider/PacketTunnelProvider.swift +199 -0
- package/ios/PacketTunnelProvider/README.md +106 -0
- package/lib/module/NativeOpenvpn.js +5 -0
- package/lib/module/NativeOpenvpn.js.map +1 -0
- package/lib/module/OpenVPNClient.js +185 -0
- package/lib/module/OpenVPNClient.js.map +1 -0
- package/lib/module/errors.js +13 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/reconnect.js +51 -0
- package/lib/module/reconnect.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/plugin/src/android/index.d.ts +5 -0
- package/lib/typescript/plugin/src/android/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidService.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidService.d.ts.map +1 -0
- package/lib/typescript/plugin/src/index.d.ts +6 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/index.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts.map +1 -0
- package/lib/typescript/plugin/src/types.d.ts +14 -0
- package/lib/typescript/plugin/src/types.d.ts.map +1 -0
- package/lib/typescript/src/NativeOpenvpn.d.ts +41 -0
- package/lib/typescript/src/NativeOpenvpn.d.ts.map +1 -0
- package/lib/typescript/src/OpenVPNClient.d.ts +37 -0
- package/lib/typescript/src/OpenVPNClient.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +9 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/reconnect.d.ts +23 -0
- package/lib/typescript/src/reconnect.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +41 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +193 -0
- package/plugin/build/android/index.d.ts +4 -0
- package/plugin/build/android/index.js +24 -0
- package/plugin/build/android/withAndroidAarCheck.d.ts +4 -0
- package/plugin/build/android/withAndroidAarCheck.js +60 -0
- package/plugin/build/android/withAndroidLegacyPackaging.d.ts +4 -0
- package/plugin/build/android/withAndroidLegacyPackaging.js +18 -0
- package/plugin/build/android/withAndroidMinSdk.d.ts +4 -0
- package/plugin/build/android/withAndroidMinSdk.js +13 -0
- package/plugin/build/android/withAndroidNotificationIcon.d.ts +4 -0
- package/plugin/build/android/withAndroidNotificationIcon.js +64 -0
- package/plugin/build/android/withAndroidPermissions.d.ts +4 -0
- package/plugin/build/android/withAndroidPermissions.js +30 -0
- package/plugin/build/android/withAndroidService.d.ts +4 -0
- package/plugin/build/android/withAndroidService.js +40 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +18 -0
- package/plugin/build/ios/index.d.ts +4 -0
- package/plugin/build/ios/index.js +15 -0
- package/plugin/build/ios/withIosDeploymentTarget.d.ts +4 -0
- package/plugin/build/ios/withIosDeploymentTarget.js +28 -0
- package/plugin/build/ios/withIosEntitlements.d.ts +4 -0
- package/plugin/build/ios/withIosEntitlements.js +15 -0
- package/plugin/build/ios/withIosInfoPlist.d.ts +4 -0
- package/plugin/build/ios/withIosInfoPlist.js +14 -0
- package/plugin/build/types.d.ts +13 -0
- package/plugin/build/types.js +2 -0
- package/src/NativeOpenvpn.ts +46 -0
- package/src/OpenVPNClient.ts +239 -0
- package/src/errors.ts +29 -0
- package/src/index.ts +12 -0
- package/src/reconnect.ts +68 -0
- package/src/types.ts +53 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const android_1 = __importDefault(require("./android"));
|
|
7
|
+
const ios_1 = __importDefault(require("./ios"));
|
|
8
|
+
const withOpenVPN = (config, props) => {
|
|
9
|
+
if (!props || typeof props.iosAppGroup !== 'string' || !props.iosAppGroup) {
|
|
10
|
+
throw new Error('react-native-ovpn: iosAppGroup is required in the plugin options. ' +
|
|
11
|
+
'Add it to your app.config.js plugins entry: ' +
|
|
12
|
+
'["react-native-ovpn", { iosAppGroup: "group.com.example.openvpn" }]');
|
|
13
|
+
}
|
|
14
|
+
config = (0, android_1.default)(config, props);
|
|
15
|
+
config = (0, ios_1.default)(config, props);
|
|
16
|
+
return config;
|
|
17
|
+
};
|
|
18
|
+
exports.default = withOpenVPN;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const withIosEntitlements_1 = __importDefault(require("./withIosEntitlements"));
|
|
7
|
+
const withIosInfoPlist_1 = __importDefault(require("./withIosInfoPlist"));
|
|
8
|
+
const withIosDeploymentTarget_1 = __importDefault(require("./withIosDeploymentTarget"));
|
|
9
|
+
const withOpenVPNIos = (config, props) => {
|
|
10
|
+
config = (0, withIosEntitlements_1.default)(config, props);
|
|
11
|
+
config = (0, withIosInfoPlist_1.default)(config, props);
|
|
12
|
+
config = (0, withIosDeploymentTarget_1.default)(config, props);
|
|
13
|
+
return config;
|
|
14
|
+
};
|
|
15
|
+
exports.default = withOpenVPNIos;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
4
|
+
const TARGET = '13.0';
|
|
5
|
+
const withIosDeploymentTarget = (config) => {
|
|
6
|
+
return (0, config_plugins_1.withXcodeProject)(config, async (config) => {
|
|
7
|
+
const project = config.modResults;
|
|
8
|
+
const section = project.pbxXCBuildConfigurationSection();
|
|
9
|
+
for (const key of Object.keys(section)) {
|
|
10
|
+
const buildConfig = section[key];
|
|
11
|
+
if (!buildConfig || typeof buildConfig !== 'object')
|
|
12
|
+
continue;
|
|
13
|
+
const settings = buildConfig.buildSettings;
|
|
14
|
+
if (!settings)
|
|
15
|
+
continue;
|
|
16
|
+
const current = settings.IPHONEOS_DEPLOYMENT_TARGET;
|
|
17
|
+
if (typeof current === 'undefined') {
|
|
18
|
+
settings.IPHONEOS_DEPLOYMENT_TARGET = TARGET;
|
|
19
|
+
}
|
|
20
|
+
else if (typeof current === 'string' &&
|
|
21
|
+
parseFloat(current) < parseFloat(TARGET)) {
|
|
22
|
+
settings.IPHONEOS_DEPLOYMENT_TARGET = TARGET;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
exports.default = withIosDeploymentTarget;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
4
|
+
const KEY = 'com.apple.security.application-groups';
|
|
5
|
+
const withIosEntitlements = (config, props) => {
|
|
6
|
+
return (0, config_plugins_1.withEntitlementsPlist)(config, async (config) => {
|
|
7
|
+
const groups = config.modResults[KEY] ?? [];
|
|
8
|
+
if (!groups.includes(props.iosAppGroup)) {
|
|
9
|
+
groups.push(props.iosAppGroup);
|
|
10
|
+
}
|
|
11
|
+
config.modResults[KEY] = groups;
|
|
12
|
+
return config;
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
exports.default = withIosEntitlements;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
4
|
+
const withIosInfoPlist = (config, props) => {
|
|
5
|
+
return (0, config_plugins_1.withInfoPlist)(config, async (config) => {
|
|
6
|
+
config.modResults.OpenVPNAppGroupIdentifier =
|
|
7
|
+
props.iosAppGroup;
|
|
8
|
+
if (props.iosExtensionBundleIdentifier) {
|
|
9
|
+
config.modResults.OpenVPNExtensionBundleIdentifier = props.iosExtensionBundleIdentifier;
|
|
10
|
+
}
|
|
11
|
+
return config;
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
exports.default = withIosInfoPlist;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type OpenVPNPluginProps = {
|
|
2
|
+
/** Required. App Group identifier shared by host and extension on iOS. */
|
|
3
|
+
iosAppGroup: string;
|
|
4
|
+
/** Optional. If the extension target's bundle id doesn't follow the default
|
|
5
|
+
* `<host>.OpenVPNTunnel` convention, set it here so the host knows what to
|
|
6
|
+
* start. */
|
|
7
|
+
iosExtensionBundleIdentifier?: string;
|
|
8
|
+
/** Optional. Path (relative to the consumer's project root) to a small
|
|
9
|
+
* notification icon to use on Android. PNG or vector drawable. */
|
|
10
|
+
androidNotificationIcon?: string;
|
|
11
|
+
/** Optional. Override the default notification channel name on Android. */
|
|
12
|
+
androidNotificationChannelName?: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type NativeNotificationConfig = {
|
|
5
|
+
title?: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
smallIcon?: string;
|
|
8
|
+
channelId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type NativeConnectParams = {
|
|
12
|
+
config: string;
|
|
13
|
+
username: string;
|
|
14
|
+
password: string;
|
|
15
|
+
killSwitch: boolean;
|
|
16
|
+
dns: string[];
|
|
17
|
+
allowedApps: string[];
|
|
18
|
+
disallowedApps: string[];
|
|
19
|
+
notification: NativeNotificationConfig;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type NativeStatus = {
|
|
23
|
+
state: string;
|
|
24
|
+
connectedSince?: number;
|
|
25
|
+
server?: string;
|
|
26
|
+
localIp?: string;
|
|
27
|
+
remoteIp?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type NativeStats = {
|
|
31
|
+
bytesIn: number;
|
|
32
|
+
bytesOut: number;
|
|
33
|
+
durationMs: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface Spec extends TurboModule {
|
|
37
|
+
requestPermission(): Promise<boolean>;
|
|
38
|
+
connect(params: NativeConnectParams): Promise<void>;
|
|
39
|
+
disconnect(): Promise<void>;
|
|
40
|
+
getStatus(): Promise<NativeStatus>;
|
|
41
|
+
getStats(): Promise<NativeStats>;
|
|
42
|
+
addListener(eventName: string): void;
|
|
43
|
+
removeListeners(count: number): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('Openvpn');
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { EmitterSubscription } from 'react-native';
|
|
2
|
+
import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
3
|
+
import NativeOpenvpn from './NativeOpenvpn';
|
|
4
|
+
import type { Stats, Status, VPNState } from './types';
|
|
5
|
+
import { ERROR_CODES, HARD_ERROR_CODES, OpenVPNError } from './errors';
|
|
6
|
+
import type { ErrorCode } from './errors';
|
|
7
|
+
import type { ConnectOptions, ReconnectingEvent } from './types';
|
|
8
|
+
import { Scheduler } from './reconnect';
|
|
9
|
+
import type { NativeConnectParams } from './NativeOpenvpn';
|
|
10
|
+
|
|
11
|
+
type Listener<T> = (payload: T) => void;
|
|
12
|
+
|
|
13
|
+
type EventMap = {
|
|
14
|
+
state: VPNState;
|
|
15
|
+
stats: Stats;
|
|
16
|
+
log: string;
|
|
17
|
+
error: OpenVPNError;
|
|
18
|
+
reconnecting: ReconnectingEvent;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// NativeEventEmitter requires a NativeModule with addListener/removeListeners.
|
|
22
|
+
// In tests the TurboModule mock satisfies this shape.
|
|
23
|
+
type NativeModuleLike = {
|
|
24
|
+
addListener: (eventName: string) => void;
|
|
25
|
+
removeListeners: (count: number) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class OpenVPNClient {
|
|
29
|
+
private readonly emitter: NativeEventEmitter;
|
|
30
|
+
private readonly listeners: {
|
|
31
|
+
[K in keyof EventMap]: Set<Listener<EventMap[K]>>;
|
|
32
|
+
} = {
|
|
33
|
+
state: new Set(),
|
|
34
|
+
stats: new Set(),
|
|
35
|
+
log: new Set(),
|
|
36
|
+
error: new Set(),
|
|
37
|
+
reconnecting: new Set(),
|
|
38
|
+
};
|
|
39
|
+
private subscriptions: EmitterSubscription[] = [];
|
|
40
|
+
|
|
41
|
+
private currentConnect: {
|
|
42
|
+
resolve: () => void;
|
|
43
|
+
reject: (err: OpenVPNError) => void;
|
|
44
|
+
} | null = null;
|
|
45
|
+
|
|
46
|
+
private userRequestedDisconnect = false;
|
|
47
|
+
private hardErrorPending = false;
|
|
48
|
+
private scheduler: Scheduler | null = null;
|
|
49
|
+
private lastParams: NativeConnectParams | null = null;
|
|
50
|
+
private lastReconnectOpts: Required<import('./types').ReconnectOptions> = {
|
|
51
|
+
maxRetries: 5,
|
|
52
|
+
baseDelayMs: 1000,
|
|
53
|
+
maxDelayMs: 60000,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
// On iOS (including the jest defaultPlatform) NativeEventEmitter requires
|
|
58
|
+
// a non-null module. Use the imported TurboModule instance (or native
|
|
59
|
+
// registry) as that module — it always exposes addListener/removeListeners.
|
|
60
|
+
const nativeModule: NativeModuleLike =
|
|
61
|
+
(NativeModules.Openvpn as NativeModuleLike | undefined) ??
|
|
62
|
+
(NativeOpenvpn as unknown as NativeModuleLike);
|
|
63
|
+
// NativeModule type is not exported from react-native, cast via any.
|
|
64
|
+
|
|
65
|
+
this.emitter = new NativeEventEmitter(nativeModule as any);
|
|
66
|
+
|
|
67
|
+
this.subscriptions.push(
|
|
68
|
+
this.emitter.addListener('OpenVpn:state', (s: unknown) => {
|
|
69
|
+
const state = s as VPNState;
|
|
70
|
+
this.emit('state', state);
|
|
71
|
+
if (state === 'connected') {
|
|
72
|
+
this.settleConnect(null, true);
|
|
73
|
+
if (this.scheduler) {
|
|
74
|
+
this.scheduler.stop();
|
|
75
|
+
this.scheduler = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (state === 'disconnected') this.handleDisconnect();
|
|
79
|
+
}),
|
|
80
|
+
this.emitter.addListener('OpenVpn:stats', (s: unknown) => {
|
|
81
|
+
this.emit('stats', s as Stats);
|
|
82
|
+
}),
|
|
83
|
+
this.emitter.addListener('OpenVpn:log', (line: unknown) => {
|
|
84
|
+
this.emit('log', line as string);
|
|
85
|
+
}),
|
|
86
|
+
this.emitter.addListener('OpenVpn:error', (raw: unknown) => {
|
|
87
|
+
const r = raw as { code: string; nativeMessage?: string };
|
|
88
|
+
const code = (ERROR_CODES as readonly string[]).includes(r.code)
|
|
89
|
+
? (r.code as ErrorCode)
|
|
90
|
+
: 'NATIVE_ERROR';
|
|
91
|
+
const err = new OpenVPNError(code, r.nativeMessage);
|
|
92
|
+
this.emit('error', err);
|
|
93
|
+
if (HARD_ERROR_CODES.has(code)) {
|
|
94
|
+
this.hardErrorPending = true;
|
|
95
|
+
if (this.currentConnect) this.settleConnect(err, false);
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
on<K extends keyof EventMap>(event: K, fn: Listener<EventMap[K]>): void {
|
|
102
|
+
this.listeners[event].add(fn);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
off<K extends keyof EventMap>(event: K, fn: Listener<EventMap[K]>): void {
|
|
106
|
+
this.listeners[event].delete(fn);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
removeAllListeners(): void {
|
|
110
|
+
for (const key of Object.keys(this.listeners) as (keyof EventMap)[]) {
|
|
111
|
+
this.listeners[key].clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
protected emit<K extends keyof EventMap>(
|
|
116
|
+
event: K,
|
|
117
|
+
payload: EventMap[K]
|
|
118
|
+
): void {
|
|
119
|
+
for (const fn of this.listeners[event]) fn(payload);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
dispose(): void {
|
|
123
|
+
for (const sub of this.subscriptions) sub.remove();
|
|
124
|
+
this.subscriptions = [];
|
|
125
|
+
this.removeAllListeners();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async requestPermission(): Promise<boolean> {
|
|
129
|
+
return NativeOpenvpn.requestPermission();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getStatus(): Promise<Status> {
|
|
133
|
+
const raw = await NativeOpenvpn.getStatus();
|
|
134
|
+
return raw as Status;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getStats(): Promise<Stats> {
|
|
138
|
+
return NativeOpenvpn.getStats();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async connect(options: ConnectOptions): Promise<void> {
|
|
142
|
+
if (this.currentConnect) {
|
|
143
|
+
throw new OpenVPNError(
|
|
144
|
+
'NATIVE_ERROR',
|
|
145
|
+
'connect() called while already connecting'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (this.scheduler) {
|
|
149
|
+
this.scheduler.stop();
|
|
150
|
+
this.scheduler = null;
|
|
151
|
+
}
|
|
152
|
+
this.userRequestedDisconnect = false;
|
|
153
|
+
this.hardErrorPending = false;
|
|
154
|
+
|
|
155
|
+
const params: NativeConnectParams = {
|
|
156
|
+
config: options.config,
|
|
157
|
+
username: options.username,
|
|
158
|
+
password: options.password,
|
|
159
|
+
killSwitch: options.killSwitch ?? false,
|
|
160
|
+
dns: options.dns ?? [],
|
|
161
|
+
allowedApps: options.allowedApps ?? [],
|
|
162
|
+
disallowedApps: options.disallowedApps ?? [],
|
|
163
|
+
notification: options.notification ?? {},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
this.lastParams = params;
|
|
167
|
+
this.lastReconnectOpts = {
|
|
168
|
+
maxRetries: options.reconnect?.maxRetries ?? 5,
|
|
169
|
+
baseDelayMs: options.reconnect?.baseDelayMs ?? 1000,
|
|
170
|
+
maxDelayMs: options.reconnect?.maxDelayMs ?? 60000,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return new Promise<void>((resolve, reject) => {
|
|
174
|
+
this.currentConnect = { resolve, reject };
|
|
175
|
+
NativeOpenvpn.connect(params).catch((nativeErr: Error) => {
|
|
176
|
+
this.settleConnect(
|
|
177
|
+
new OpenVPNError('NATIVE_ERROR', nativeErr.message),
|
|
178
|
+
false
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async disconnect(): Promise<void> {
|
|
185
|
+
this.userRequestedDisconnect = true;
|
|
186
|
+
if (this.scheduler) {
|
|
187
|
+
this.scheduler.stop();
|
|
188
|
+
this.scheduler = null;
|
|
189
|
+
}
|
|
190
|
+
await NativeOpenvpn.disconnect();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private settleConnect(err: OpenVPNError | null, resolved: boolean): void {
|
|
194
|
+
const pending = this.currentConnect;
|
|
195
|
+
if (!pending) return;
|
|
196
|
+
this.currentConnect = null;
|
|
197
|
+
if (resolved) pending.resolve();
|
|
198
|
+
else if (err) pending.reject(err);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private handleDisconnect(): void {
|
|
202
|
+
if (this.userRequestedDisconnect || this.hardErrorPending) return;
|
|
203
|
+
if (!this.lastParams) return;
|
|
204
|
+
// Immediately overwrite the just-emitted 'disconnected' so the UI never
|
|
205
|
+
// paints an enabled Connect button between auto-retry attempts. React's
|
|
206
|
+
// automatic batching collapses both setState calls in this tick.
|
|
207
|
+
this.emit('state', 'reconnecting');
|
|
208
|
+
if (this.scheduler) {
|
|
209
|
+
// We're already in a retry loop and just heard another drop —
|
|
210
|
+
// tell the scheduler the most-recent attempt failed.
|
|
211
|
+
this.scheduler.notifyFailure();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.scheduler = new Scheduler({
|
|
215
|
+
maxRetries: this.lastReconnectOpts.maxRetries,
|
|
216
|
+
baseDelayMs: this.lastReconnectOpts.baseDelayMs,
|
|
217
|
+
maxDelayMs: this.lastReconnectOpts.maxDelayMs,
|
|
218
|
+
onAttempt: ({ attempt, delayMs }) => {
|
|
219
|
+
this.emit('state', 'reconnecting');
|
|
220
|
+
this.emit('reconnecting', { attempt, delayMs });
|
|
221
|
+
// Drive retries off state events, not the connect() Promise — the
|
|
222
|
+
// catch here only logs; the eventual 'disconnected' event drives
|
|
223
|
+
// notifyFailure via handleDisconnect.
|
|
224
|
+
NativeOpenvpn.connect(this.lastParams!).catch((nativeErr: Error) => {
|
|
225
|
+
this.emit(
|
|
226
|
+
'error',
|
|
227
|
+
new OpenVPNError('NATIVE_ERROR', nativeErr.message)
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
onExhausted: () => {
|
|
232
|
+
this.emit('error', new OpenVPNError('RECONNECT_EXHAUSTED'));
|
|
233
|
+
this.emit('state', 'disconnected');
|
|
234
|
+
this.scheduler = null;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
this.scheduler.start();
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const ERROR_CODES = [
|
|
2
|
+
'PERMISSION_DENIED',
|
|
3
|
+
'INVALID_CONFIG',
|
|
4
|
+
'AUTH_FAILED',
|
|
5
|
+
'TLS_FAILED',
|
|
6
|
+
'NETWORK_UNREACHABLE',
|
|
7
|
+
'RECONNECT_EXHAUSTED',
|
|
8
|
+
'NATIVE_ERROR',
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
12
|
+
|
|
13
|
+
export class OpenVPNError extends Error {
|
|
14
|
+
readonly code: ErrorCode;
|
|
15
|
+
readonly nativeMessage?: string;
|
|
16
|
+
|
|
17
|
+
constructor(code: ErrorCode, nativeMessage?: string) {
|
|
18
|
+
super(nativeMessage ? `${code}: ${nativeMessage}` : code);
|
|
19
|
+
this.name = 'OpenVPNError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.nativeMessage = nativeMessage;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const HARD_ERROR_CODES: ReadonlySet<ErrorCode> = new Set([
|
|
26
|
+
'AUTH_FAILED',
|
|
27
|
+
'INVALID_CONFIG',
|
|
28
|
+
'PERMISSION_DENIED',
|
|
29
|
+
]);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { OpenVPNClient } from './OpenVPNClient';
|
|
2
|
+
export { OpenVPNError, ERROR_CODES } from './errors';
|
|
3
|
+
export type { ErrorCode } from './errors';
|
|
4
|
+
export type {
|
|
5
|
+
VPNState,
|
|
6
|
+
ConnectOptions,
|
|
7
|
+
ReconnectOptions,
|
|
8
|
+
NotificationOptions,
|
|
9
|
+
Status,
|
|
10
|
+
Stats,
|
|
11
|
+
ReconnectingEvent,
|
|
12
|
+
} from './types';
|
package/src/reconnect.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function calculateDelay(
|
|
2
|
+
attempt: number,
|
|
3
|
+
baseDelayMs: number,
|
|
4
|
+
maxDelayMs: number
|
|
5
|
+
): number {
|
|
6
|
+
const raw = baseDelayMs * Math.pow(2, attempt);
|
|
7
|
+
return Math.min(raw, maxDelayMs);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SchedulerOptions = {
|
|
11
|
+
maxRetries: number;
|
|
12
|
+
baseDelayMs: number;
|
|
13
|
+
maxDelayMs: number;
|
|
14
|
+
onAttempt: (info: { attempt: number; delayMs: number }) => void;
|
|
15
|
+
onExhausted: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class Scheduler {
|
|
19
|
+
private readonly opts: SchedulerOptions;
|
|
20
|
+
private attempt = 0;
|
|
21
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
+
private started = false;
|
|
23
|
+
|
|
24
|
+
constructor(opts: SchedulerOptions) {
|
|
25
|
+
this.opts = opts;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start(): void {
|
|
29
|
+
if (this.started) return;
|
|
30
|
+
this.started = true;
|
|
31
|
+
this.attempt = 0;
|
|
32
|
+
this.scheduleNext();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
stop(): void {
|
|
36
|
+
this.started = false;
|
|
37
|
+
if (this.timer !== null) {
|
|
38
|
+
clearTimeout(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
notifyFailure(): void {
|
|
44
|
+
if (!this.started) return;
|
|
45
|
+
if (this.attempt >= this.opts.maxRetries) {
|
|
46
|
+
this.stop();
|
|
47
|
+
this.opts.onExhausted();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.scheduleNext();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private scheduleNext(): void {
|
|
54
|
+
if (this.timer !== null) {
|
|
55
|
+
clearTimeout(this.timer);
|
|
56
|
+
}
|
|
57
|
+
const delayMs = calculateDelay(
|
|
58
|
+
this.attempt,
|
|
59
|
+
this.opts.baseDelayMs,
|
|
60
|
+
this.opts.maxDelayMs
|
|
61
|
+
);
|
|
62
|
+
this.timer = setTimeout(() => {
|
|
63
|
+
this.timer = null;
|
|
64
|
+
this.attempt += 1;
|
|
65
|
+
this.opts.onAttempt({ attempt: this.attempt, delayMs });
|
|
66
|
+
}, delayMs);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type VPNState =
|
|
2
|
+
| 'idle'
|
|
3
|
+
| 'connecting'
|
|
4
|
+
| 'reconnecting'
|
|
5
|
+
| 'connected'
|
|
6
|
+
| 'disconnecting'
|
|
7
|
+
| 'disconnected'
|
|
8
|
+
| 'error';
|
|
9
|
+
|
|
10
|
+
export type NotificationOptions = {
|
|
11
|
+
title?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
smallIcon?: string;
|
|
14
|
+
channelId?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ReconnectOptions = {
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
baseDelayMs?: number;
|
|
20
|
+
maxDelayMs?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ConnectOptions = {
|
|
24
|
+
config: string;
|
|
25
|
+
username: string;
|
|
26
|
+
password: string;
|
|
27
|
+
reconnect?: ReconnectOptions;
|
|
28
|
+
killSwitch?: boolean;
|
|
29
|
+
dns?: string[];
|
|
30
|
+
allowedApps?: string[];
|
|
31
|
+
disallowedApps?: string[];
|
|
32
|
+
notification?: NotificationOptions;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type Status = {
|
|
36
|
+
state: VPNState;
|
|
37
|
+
connectedSince?: number;
|
|
38
|
+
server?: string;
|
|
39
|
+
localIp?: string;
|
|
40
|
+
remoteIp?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type Stats = {
|
|
44
|
+
bytesIn: number;
|
|
45
|
+
bytesOut: number;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ReconnectingEvent = {
|
|
50
|
+
attempt: number;
|
|
51
|
+
/** Delay in ms that elapsed BEFORE this attempt was made (not the next wait). */
|
|
52
|
+
delayMs: number;
|
|
53
|
+
};
|