Android shadowsocks code added
This commit is contained in:
parent
ccdd433e35
commit
929bcf03a0
92 changed files with 39982 additions and 1702 deletions
|
@ -15,7 +15,8 @@
|
||||||
<!-- %%INSERT_FEATURES -->
|
<!-- %%INSERT_FEATURES -->
|
||||||
|
|
||||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||||
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
|
|
||||||
|
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:hardwareAccelerated="true" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
|
||||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/splashScreenTheme">
|
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/splashScreenTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
@ -76,17 +77,21 @@
|
||||||
<!-- extract android style -->
|
<!-- extract android style -->
|
||||||
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
|
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service android:name=".VPNService" android:process=":QtOnlyProcess">
|
||||||
<service android:name=".VPNService"
|
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||||
android:process=":QtOnlyProcess"
|
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||||
<intent-filter>
|
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||||
<action android:name="android.net.VpnService"/>
|
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
|
||||||
</intent-filter>
|
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
|
||||||
|
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
|
||||||
|
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
|
||||||
|
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
|
||||||
|
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
|
||||||
|
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
|
||||||
</service>
|
</service>
|
||||||
|
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper">
|
||||||
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper"
|
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
|
||||||
</service>
|
</service>
|
||||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.github.shadowsocks.aidl;
|
|
||||||
|
|
||||||
import com.github.shadowsocks.aidl.TrafficStats;
|
|
||||||
|
|
||||||
//"oneway" unexpected. xinlake
|
|
||||||
interface IShadowsocksServiceCallback {
|
|
||||||
oneway void stateChanged(int state, String profileName, String msg);
|
|
||||||
oneway void trafficUpdated(long profileId, in TrafficStats stats);
|
|
||||||
// Traffic data has persisted to database, listener should refetch their data from database
|
|
||||||
oneway void trafficPersisted(long profileId);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package com.github.shadowsocks.aidl;
|
|
||||||
|
|
||||||
parcelable TrafficStats;
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.shadowsocks.aidl;
|
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||||
|
|
||||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback;
|
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback;
|
||||||
|
|
||||||
interface IShadowsocksService {
|
interface IShadowsocksService {
|
||||||
int getState();
|
int getState();
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||||
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats;
|
||||||
|
|
||||||
|
//"oneway" unexpected. xinlake
|
||||||
|
interface IShadowsocksServiceCallback {
|
||||||
|
oneway void stateChanged(int state, String profileName, String msg);
|
||||||
|
oneway void trafficUpdated(long profileId, in TrafficStats stats);
|
||||||
|
// Traffic data has persisted to database, listener should refetch their data from database
|
||||||
|
oneway void trafficPersisted(long profileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//oneway interface IShadowsocksServiceCallback {
|
||||||
|
// void stateChanged(int state, String profileName, String msg);
|
||||||
|
// void trafficUpdated(long profileId, in TrafficStats stats);
|
||||||
|
// // Traffic data has persisted to database, listener should refetch their data from database
|
||||||
|
// void trafficPersisted(long profileId);
|
||||||
|
//}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||||
|
|
||||||
|
parcelable TrafficStats;
|
10377
client/android/assets/acl/bypass-china.acl
Normal file
10377
client/android/assets/acl/bypass-china.acl
Normal file
File diff suppressed because it is too large
Load diff
10391
client/android/assets/acl/bypass-lan-china.acl
Normal file
10391
client/android/assets/acl/bypass-lan-china.acl
Normal file
File diff suppressed because it is too large
Load diff
|
@ -7,13 +7,10 @@
|
||||||
127.0.0.0/8
|
127.0.0.0/8
|
||||||
169.254.0.0/16
|
169.254.0.0/16
|
||||||
172.16.0.0/12
|
172.16.0.0/12
|
||||||
192.0.0.0/24
|
192.0.0.0/29
|
||||||
192.0.2.0/24
|
192.0.2.0/24
|
||||||
192.31.196.0/24
|
|
||||||
192.52.193.0/24
|
|
||||||
192.88.99.0/24
|
192.88.99.0/24
|
||||||
192.168.0.0/16
|
192.168.0.0/16
|
||||||
192.175.48.0/24
|
|
||||||
198.18.0.0/15
|
198.18.0.0/15
|
||||||
198.51.100.0/24
|
198.51.100.0/24
|
||||||
203.0.113.0/24
|
203.0.113.0/24
|
||||||
|
|
5245
client/android/assets/acl/china-list.acl
Normal file
5245
client/android/assets/acl/china-list.acl
Normal file
File diff suppressed because it is too large
Load diff
5492
client/android/assets/acl/gfwlist.acl
Normal file
5492
client/android/assets/acl/gfwlist.acl
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,8 @@
|
||||||
|
apply plugin: 'com.github.ben-manes.versions'
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext{
|
ext{
|
||||||
kotlin_version = "1.5.0"
|
kotlin_version = "1.4.30-M1"
|
||||||
// for libwg
|
// for libwg
|
||||||
appcompatVersion = '1.1.0'
|
appcompatVersion = '1.1.0'
|
||||||
annotationsVersion = '1.0.1'
|
annotationsVersion = '1.0.1'
|
||||||
|
@ -19,6 +21,8 @@ buildscript {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||||
|
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
||||||
|
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
}
|
}
|
||||||
|
@ -34,24 +38,44 @@ apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
implementation 'androidx.core:core-ktx:1.1.0'
|
implementation 'androidx.core:core-ktx:1.1.0'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02"
|
//implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02"
|
||||||
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
||||||
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||||
//ss
|
//ss
|
||||||
implementation "androidx.preference:preference:1.1.0"
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||||
implementation "androidx.work:work-runtime-ktx:2.3.4"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||||
|
//implementation "androidx.core:core-ktx:1.2.0"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||||
implementation "androidx.room:room-runtime:2.2.5" // runtime
|
implementation "androidx.room:room-runtime:2.2.5" // runtime
|
||||||
implementation "dnsjava:dnsjava:2.1.9"
|
implementation "androidx.preference:preference:1.1.0"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:2.3.4"
|
||||||
|
implementation "androidx.browser:browser:1.3.0-alpha01"
|
||||||
|
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||||
|
implementation "com.google.android.material:material:1.2.0-alpha05"
|
||||||
implementation "com.google.code.gson:gson:2.8.5"
|
implementation "com.google.code.gson:gson:2.8.5"
|
||||||
|
implementation "dnsjava:dnsjava:2.1.9"
|
||||||
implementation "org.connectbot.jsocks:jsocks:1.0.0"
|
implementation "org.connectbot.jsocks:jsocks:1.0.0"
|
||||||
annotationProcessor "androidx.room:room-compiler:2.3.0"
|
implementation "com.afollestad.material-dialogs:core:2.6.0"
|
||||||
|
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||||
|
implementation 'com.android.support:multidex:1.0.0'
|
||||||
|
api 'org.connectbot.jsocks:jsocks:1.0.0'
|
||||||
|
annotationProcessor "androidx.room:room-compiler:2.2.5"
|
||||||
|
annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.4.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -86,6 +110,7 @@ android {
|
||||||
renderscript.srcDirs = ['src']
|
renderscript.srcDirs = ['src']
|
||||||
assets.srcDirs = ['assets']
|
assets.srcDirs = ['assets']
|
||||||
jniLibs.srcDirs = ['libs']
|
jniLibs.srcDirs = ['libs']
|
||||||
|
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +139,10 @@ android {
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 30
|
||||||
versionCode 8 // Change to a higher number
|
versionCode 8 // Change to a higher number
|
||||||
versionName "2.0.8" // Change to a higher number
|
versionName "2.0.8" // Change to a higher number
|
||||||
|
|
||||||
|
javaCompileOptions.annotationProcessorOptions.arguments = [
|
||||||
|
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString()
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -136,18 +165,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// externalNativeBuild {
|
|
||||||
// cmake {
|
|
||||||
// path 'wireguard/CMakeLists.txt'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// externalNativeBuild {
|
|
||||||
// cmake {
|
|
||||||
// path 'openvpn/src/main/cpp/CMakeLists.txt'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m
|
#org.gradle.jvmargs=-Xmx2048m
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
# Gradle caching allows reusing the build artifacts from a previous
|
# Gradle caching allows reusing the build artifacts from a previous
|
||||||
# build with the same inputs. However, over time, the cache size will
|
# build with the same inputs. However, over time, the cache size will
|
||||||
|
@ -21,3 +22,7 @@ androidBuildToolsVersion=30.0.2
|
||||||
androidCompileSdkVersion=30
|
androidCompileSdkVersion=30
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
android.injected.testOnly=false
|
||||||
|
kapt.use.worker.api=false
|
||||||
|
kapt.incremental.apt=false
|
||||||
|
|
BIN
client/android/lib/shadowsocks/arm64-v8a/libredsocks.so
Normal file
BIN
client/android/lib/shadowsocks/arm64-v8a/libredsocks.so
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
client/android/lib/shadowsocks/armeabi-v7a/libredsocks.so
Normal file
BIN
client/android/lib/shadowsocks/armeabi-v7a/libredsocks.so
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
client/android/lib/shadowsocks/x86/libredsocks.so
Normal file
BIN
client/android/lib/shadowsocks/x86/libredsocks.so
Normal file
Binary file not shown.
BIN
client/android/lib/shadowsocks/x86/libss-local.so
Normal file
BIN
client/android/lib/shadowsocks/x86/libss-local.so
Normal file
Binary file not shown.
BIN
client/android/lib/shadowsocks/x86/libtun2socks.so
Normal file
BIN
client/android/lib/shadowsocks/x86/libtun2socks.so
Normal file
Binary file not shown.
BIN
client/android/lib/shadowsocks/x86_64/libredsocks.so
Normal file
BIN
client/android/lib/shadowsocks/x86_64/libredsocks.so
Normal file
Binary file not shown.
BIN
client/android/lib/shadowsocks/x86_64/libss-local.so
Normal file
BIN
client/android/lib/shadowsocks/x86_64/libss-local.so
Normal file
Binary file not shown.
BIN
client/android/lib/shadowsocks/x86_64/libtun2socks.so
Normal file
BIN
client/android/lib/shadowsocks/x86_64/libtun2socks.so
Normal file
Binary file not shown.
5215
client/android/res/raw/china_ip_list.txt
Normal file
5215
client/android/res/raw/china_ip_list.txt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,113 +1,169 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Shadowsocks</string>
|
<string name="app_name">shadowsocks</string>
|
||||||
<string name="send_email">Send email</string>
|
|
||||||
|
|
||||||
<!-- ssplugin -->
|
<string name="service_mode_vpn">VPN</string>
|
||||||
<string name="proxy_cat">Server Settings</string>
|
<string name="speed">%s/s</string>
|
||||||
<string name="feature_cat">Feature Settings</string>
|
|
||||||
<string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
|
|
||||||
<string name="yes">Yes</string>
|
<string name="quick_toggle">"Switch"</string>
|
||||||
<string name="no">No</string>
|
<string name="remote_dns">"Remote DNS"</string>
|
||||||
<string name="apply">Apply</string>
|
<string name="stat_summary">"Upload: \t%3$s\t↑\t%1$s
|
||||||
<string name="file_manager_missing">File Explorer Missing</string>
|
Download: \t%4$s\t↓\t%2$s"</string>
|
||||||
<string name="browse">Browse…</string>
|
<string name="connection_test_testing">"Testing…"</string>
|
||||||
|
<string name="connection_test_available">"Connection successful: HTTPS handshake delay %d milliseconds"</string>
|
||||||
|
<string name="connection_test_error">"Failed: %s"</string>
|
||||||
|
<string name="connection_test_fail">"No Internet Connection"</string>
|
||||||
|
<string name="connection_test_error_status_code">"Invalid status code (#%d) "</string>
|
||||||
|
|
||||||
<!-- proxy category -->
|
<!-- proxy category -->
|
||||||
<string name="profile_name">Profile Name</string>
|
<string name="profile_name">"Profile name"</string>
|
||||||
<string name="proxy">Server</string>
|
<string name="proxy">"Server"</string>
|
||||||
<string name="remote_port">Remote Port</string>
|
<string name="remote_port">"Remote Port"</string>
|
||||||
<string name="sitekey">Password</string>
|
<string name="sitekey">"Password"</string>
|
||||||
<string name="enc_method">Encrypt Method</string>
|
<string name="enc_method">"Encryption"</string>
|
||||||
|
|
||||||
<!-- feature category -->
|
<!-- feature category -->
|
||||||
<string name="ipv6">IPv6 Route</string>
|
<string name="ipv6">"IPv6 routing"</string>
|
||||||
<string name="ipv6_summary">Redirect IPv6 traffic to remote</string>
|
<string name="ipv6_summary">"Forward IPv6 traffic to remote server"</string>
|
||||||
<string name="on">On</string>
|
<string name="route_list">"Routing"</string>
|
||||||
<string name="off">Off</string>
|
<string name="route_entry_gfwlist">"GFW List"</string>
|
||||||
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
|
<string name="proxied_apps">"Proxied VPN"</string>
|
||||||
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s < 3.7.1</string>
|
<string name="proxied_apps_summary">"Allow some apps to bypass VPN"</string>
|
||||||
<string name="tcp_fastopen_failure">Toggle failed</string>
|
<string name="on">"On"</string>
|
||||||
<string name="udp_dns">Send DNS over UDP</string>
|
<string name="bypass_apps">"Bypass"</string>
|
||||||
<string name="udp_dns_summary">Requires UDP forwarding on server side</string>
|
<string name="bypass_apps_summary">"Bypass selected apps"</string>
|
||||||
|
<string name="auto_connect">"Auto connect"</string>
|
||||||
|
<string name="auto_connect_summary">"Allow Shadowsocks to start with the system"</string>
|
||||||
|
<string name="tcp_fastopen_summary">"Switching may require ROOT permissions"</string>
|
||||||
|
<string name="tcp_fastopen_summary_unsupported">"Unsupported kernel version: %s < 3.7.1"</string>
|
||||||
|
<string name="udp_dns">"Using UDP DNS"</string>
|
||||||
|
<string name="udp_dns_summary">"Requires remote server to support UDP forwarding"</string>
|
||||||
|
|
||||||
<!-- notification category -->
|
<!-- notification category -->
|
||||||
<string name="service_vpn">VPN Service</string>
|
<string name="forward_success">"Background service has started running. "</string>
|
||||||
<string name="forward_success">Shadowsocks started.</string>
|
<string name="invalid_server">"Invalid server name"</string>
|
||||||
<string name="invalid_server">Invalid server name</string>
|
<string name="service_failed">"Unable to connect to remote server"</string>
|
||||||
<string name="service_failed">Failed to connect the remote server</string>
|
<string name="stop">"Stop"</string>
|
||||||
<string name="stop">Stop</string>
|
<string name="stopping">"stopping…"</string>
|
||||||
<string name="stopping">Shutting down…</string>
|
<string name="vpn_error">"Background service failed to start: %s"</string>
|
||||||
<string name="vpn_error">%s</string>
|
<string name="reboot_required">"VPN service failed to start. You may need to restart your device."</string>
|
||||||
<string name="vpn_permission_denied">Permission denied to create a VPN service</string>
|
<string name="profile_invalid_input">"No valid configuration file found."</string>
|
||||||
<string name="reboot_required">Failed to start VPN service. You might need to reboot your device.</string>
|
|
||||||
<string name="profile_invalid_input">No valid profile data found.</string>
|
|
||||||
|
|
||||||
<!-- alert category -->
|
<!-- alert category -->
|
||||||
<string name="profile_empty">Please select a profile</string>
|
<string name="profile_empty">"Please select a profile"</string>
|
||||||
<string name="proxy_empty">Proxy/Password should not be empty</string>
|
<string name="proxy_empty">"The proxy server address and password cannot be empty"</string>
|
||||||
<string name="connect">Connect</string>
|
<string name="connect">"Connect"</string>
|
||||||
|
|
||||||
<!-- menu category -->
|
<!-- menu category -->
|
||||||
<string name="profiles">Profiles</string>
|
<string name="profiles">"Profiles"</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">"Settings"</string>
|
||||||
<string name="about">About</string>
|
<string name="faq">"FAQ"</string>
|
||||||
<string name="about_title">Shadowsocks %s</string>
|
<string name="about">"About"</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="about_title">"Shadowsocks %s"</string>
|
||||||
<string name="share">Share</string>
|
<string name="edit">"Edit"</string>
|
||||||
<string name="add_profile">Add Profile</string>
|
<string name="share">"Share"</string>
|
||||||
<string name="action_apply_all">Apply Settings to All Profiles</string>
|
<string name="add_profile">"Add Profile"</string>
|
||||||
<string name="action_export_more">Export…</string>
|
<string name="action_apply_all">"Apply settings to all profiles"</string>
|
||||||
<string name="action_export_file">Export to file…</string>
|
<string name="action_export">"Export to clipboard"</string>
|
||||||
<string name="action_export">Export to Clipboard</string>
|
<string name="action_import">"Import from clipboard"</string>
|
||||||
<string name="action_import">Import from Clipboard</string>
|
<string name="action_export_msg">"Export to clipboard succeeded"</string>
|
||||||
<string name="action_import_file">Import from file…</string>
|
<string name="action_export_err">"Export to clipboard failed"</string>
|
||||||
<string name="action_replace_file">Replace from file…</string>
|
<string name="action_import_msg">"Import successful"</string>
|
||||||
<string name="action_export_msg">Successfully export!</string>
|
<string name="action_import_err">"Import failed"</string>
|
||||||
<string name="action_export_err">Failed to export.</string>
|
|
||||||
<string name="action_import_msg">Successfully import!</string>
|
|
||||||
<string name="action_import_err">Failed to import.</string>
|
|
||||||
<string name="action_fetch_location">Fetch location</string>
|
|
||||||
|
|
||||||
<!-- profile -->
|
<!-- profile -->
|
||||||
<string name="profile_config">Profile config</string>
|
<string name="profile_config">"Profile Config"</string>
|
||||||
<string name="delete">Remove</string>
|
<string name="delete">"Delete"</string>
|
||||||
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
|
<string name="delete_confirm_prompt">"Are you sure you want to delete this profile?"</string>
|
||||||
<string name="share_qr_nfc">QR code</string>
|
<string name="share_qr_nfc">"QR code / NFC"</string>
|
||||||
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
|
<string name="add_profile_dialog">"Add this profile for Shadowsock?"</string>
|
||||||
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
|
<string name="add_profile_methods_scan_qr_code">"Scan QR code"</string>
|
||||||
<string name="add_profile_methods_manual_settings">Manual Settings</string>
|
<plurals name="removed">
|
||||||
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
|
<item quantity="other">"%d items deleted"</item>
|
||||||
<string name="undo">Undo</string>
|
</plurals>
|
||||||
|
<string name="undo">"Undo"</string>
|
||||||
|
|
||||||
|
<!-- tasker -->
|
||||||
|
<string name="toggle_service_state">"Start service"</string>
|
||||||
|
<string name="start_service_default">"Connect to the current server"</string>
|
||||||
|
<string name="start_service">"Connect to %s"</string>
|
||||||
|
<string name="stop_service">"Switch to %s"</string>
|
||||||
|
<string name="profile_default">"Use current profile"</string>
|
||||||
|
|
||||||
<!-- status -->
|
<!-- status -->
|
||||||
<string name="connecting">Connecting…</string>
|
<string name="sent">"Send: "</string>
|
||||||
<string name="vpn_connected">Connected, tap to check connection</string>
|
<string name="received">"Received:"</string>
|
||||||
<string name="not_connected">Not connected</string>
|
|
||||||
|
|
||||||
<string name="sent">Sent</string>
|
<!-- status -->
|
||||||
<string name="received">Received</string>
|
<string name="connecting">"connecting…"</string>
|
||||||
|
<string name="vpn_connected">"Connected, click Test Connection"</string>
|
||||||
|
<string name="not_connected">"Not connected"</string>
|
||||||
|
|
||||||
|
<!-- acl -->
|
||||||
|
<string name="custom_rules">"Custom rules"</string>
|
||||||
|
<string name="action_add_rule">"Add rule…"</string>
|
||||||
|
<string name="edit_rule">"Edit rules"</string>
|
||||||
|
<string name="route_entry_all">"Global"</string>
|
||||||
|
<string name="route_entry_bypass_lan">"Bypass LAN addresses"</string>
|
||||||
|
<string name="route_entry_bypass_chn">"Bypass mainland China addresses"</string>
|
||||||
|
<string name="route_entry_bypass_lan_chn">"Bypass LAN and Mainland China addresses"</string>
|
||||||
|
<string name="route_entry_chinalist">"Proxy only for mainland China addresses"</string>
|
||||||
|
<string name="acl_rule_templates_generic">"Subnet/Domain PCRE Regular Expression"</string>
|
||||||
|
<string name="acl_rule_templates_domain">"Domain names and their subdomains"</string>
|
||||||
|
|
||||||
|
<!-- plugin -->
|
||||||
|
<string name="plugin">"Plugin"</string>
|
||||||
|
<string name="plugin_configure">"Configure…"</string>
|
||||||
|
<string name="plugin_disabled">"Disabled"</string>
|
||||||
|
<string name="plugin_unknown">"Unknown plugin %s"</string>
|
||||||
|
<string name="plugin_untrusted">"Warning: This plugin does not appear to be from a known trusted source."</string>
|
||||||
|
<string name="profile_plugin">"Plugin: %s"</string>
|
||||||
|
<string name="add_profile_scanner_permission_required">"Scanning the QR code requires permission to use the camera."</string>
|
||||||
|
|
||||||
|
<!-- notification category -->
|
||||||
|
<string name="service_vpn">"VPN service"</string>
|
||||||
|
<string name="add_profile_methods_manual_settings">"Manual setting"</string>
|
||||||
|
|
||||||
<!-- misc -->
|
<!-- misc -->
|
||||||
<string name="add_first_profile">There is no profile currently, would you like to add it now?</string>
|
<string name="advanced">"Advanced options"</string>
|
||||||
<string name="port_proxy">SOCKS5 proxy port</string>
|
|
||||||
<string name="port_local_dns">Local DNS port</string>
|
|
||||||
|
|
||||||
<string name="quick_toggle">Toggle</string>
|
<!-- misc -->
|
||||||
<string name="remote_dns">Remote DNS</string>
|
<string name="service_mode">"Service mode"</string>
|
||||||
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s\nReceived: \t%4$s\t↓\t%2$s</string>
|
<string name="service_mode_proxy">"Proxy only"</string>
|
||||||
<string name="connection_test_pending">Check Connectivity</string>
|
<string name="service_mode_transproxy">"Transparent proxy"</string>
|
||||||
<string name="connection_test_testing">Testing…</string>
|
<string name="port_proxy">"SOCKS5 proxy port"</string>
|
||||||
<string name="connection_test_available">Success: HTTPS handshake took %dms</string>
|
<string name="port_local_dns">"local DNS port"</string>
|
||||||
<string name="connection_test_error">Fail to detect internet connection: %s</string>
|
<string name="port_transproxy">"Transparent proxy port"</string>
|
||||||
<string name="connection_test_fail">Internet Unavailable</string>
|
<string name="service_proxy">"Proxy mode"</string>
|
||||||
<string name="connection_test_error_status_code">Error code: #%d</string>
|
<string name="service_transproxy">"Transparent proxy mode"</string>
|
||||||
|
<string name="vpn_permission_denied">"Insufficient permission to create VPN service"</string>
|
||||||
<string name="speed" translatable="false">%s/s</string>
|
<string name="auto_connect_summary_v24">"Allow Shadowsocks to start with the system, an always-on VPN is recommended"</string>
|
||||||
<string name="traffic" translatable="false">%1$s↑\t%2$s↓</string>
|
<string name="direct_boot_aware">"Allow toggle on lock screen"</string>
|
||||||
|
<string name="direct_boot_aware_summary">"The selected configuration information will be less secure"</string>
|
||||||
<plurals name="removed">
|
<string name="acl_rule_online_config">"Online Rules File URL"</string>
|
||||||
<item quantity="one">Removed</item>
|
<string name="action_import_file">"Import from file…"</string>
|
||||||
<item quantity="other">%d items removed</item>
|
<string name="night_mode">"Night Mode"</string>
|
||||||
</plurals>
|
<string name="night_mode_system">"System"</string>
|
||||||
|
<string name="night_mode_auto">"Auto"</string>
|
||||||
|
<string name="night_mode_on">"On"</string>
|
||||||
|
<string name="night_mode_off">"Off"</string>
|
||||||
|
<string name="send_email">"Send email"</string>
|
||||||
|
<string name="action_export_more">"Export…"</string>
|
||||||
|
<string name="action_export_file">"Export to file…"</string>
|
||||||
|
<string name="cleartext_http_warning">"HTTP clear text traffic is not secure"</string>
|
||||||
|
<string name="share_over_lan">"Share via LAN"</string>
|
||||||
|
<string name="connection_test_pending">"Check connection"</string>
|
||||||
|
<string name="file_manager_missing">"Please install a file manager such as MiXplorer"</string>
|
||||||
|
<string name="tcp_fastopen_failure">"Failed to switch"</string>
|
||||||
|
<string name="udp_fallback">"UDP configuration"</string>
|
||||||
|
<string name="action_replace_file">"Replace from file…"</string>
|
||||||
|
<string name="off">"Off"</string>
|
||||||
|
<string name="proxied_apps_mode">"model"</string>
|
||||||
|
<string name="proxy_cat">"Server settings"</string>
|
||||||
|
<string name="feature_cat">"Function settings"</string>
|
||||||
|
<string name="unsaved_changes_prompt">"Do you want to save the changes?"</string>
|
||||||
|
<string name="yes">"Yes"</string>
|
||||||
|
<string name="no">"No"</string>
|
||||||
|
<string name="apply">"Apply"</string>
|
||||||
</resources>
|
</resources>
|
|
@ -0,0 +1,132 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1000,
|
||||||
|
"identityHash": "14b379f7776710b79b9d617090efe40e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Profile",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "host",
|
||||||
|
"columnName": "host",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "remotePort",
|
||||||
|
"columnName": "remotePort",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "method",
|
||||||
|
"columnName": "method",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "remoteDns",
|
||||||
|
"columnName": "remoteDns",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "udpdns",
|
||||||
|
"columnName": "udpdns",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ipv6",
|
||||||
|
"columnName": "ipv6",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tx",
|
||||||
|
"columnName": "tx",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rx",
|
||||||
|
"columnName": "rx",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userOrder",
|
||||||
|
"columnName": "userOrder",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "KeyValuePair",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "valueType",
|
||||||
|
"columnName": "valueType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14b379f7776710b79b9d617090efe40e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "f1aab1fb633378621635c344dbc8ac7b",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "KeyValuePair",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "valueType",
|
||||||
|
"columnName": "valueType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f1aab1fb633378621635c344dbc8ac7b')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,215 +0,0 @@
|
||||||
package com.github.shadowsocks
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.LocalSocket
|
|
||||||
import android.net.LocalSocketAddress
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.VpnService
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.ParcelFileDescriptor
|
|
||||||
import android.system.ErrnoException
|
|
||||||
import android.system.Os
|
|
||||||
import com.github.shadowsocks.bg.*
|
|
||||||
import com.github.shadowsocks.net.ConcurrentLocalSocketListener
|
|
||||||
import com.github.shadowsocks.net.DefaultNetworkListener
|
|
||||||
import com.github.shadowsocks.net.HostsFile
|
|
||||||
import com.github.shadowsocks.net.Subnet
|
|
||||||
import com.github.shadowsocks.preference.DataStore
|
|
||||||
import com.github.shadowsocks.utils.printLog
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileDescriptor
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URL
|
|
||||||
import org.amnezia.vpn.R
|
|
||||||
|
|
||||||
class LocalVpnService : VpnService(), LocalDnsService.Interface {
|
|
||||||
companion object {
|
|
||||||
private const val VPN_MTU = 1500
|
|
||||||
private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
|
|
||||||
private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
|
|
||||||
private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
|
|
||||||
private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
|
|
||||||
*/
|
|
||||||
private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
|
|
||||||
}
|
|
||||||
|
|
||||||
class CloseableFd(val fd: FileDescriptor) : Closeable {
|
|
||||||
override fun close() = Os.close(fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ProtectWorker : ConcurrentLocalSocketListener(
|
|
||||||
"ShadowsocksVpnThread",
|
|
||||||
File(Core.deviceStorage.noBackupFilesDir, "protect_path")
|
|
||||||
) {
|
|
||||||
override fun acceptInternal(socket: LocalSocket) {
|
|
||||||
socket.inputStream.read()
|
|
||||||
val fd = socket.ancillaryFileDescriptors!!.single()!!
|
|
||||||
CloseableFd(fd).use {
|
|
||||||
socket.outputStream.write(if (underlyingNetwork.let { network ->
|
|
||||||
if (network != null && Build.VERSION.SDK_INT >= 23) try {
|
|
||||||
network.bindSocket(fd)
|
|
||||||
true
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// suppress ENONET (Machine is not on the network)
|
|
||||||
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
|
|
||||||
false
|
|
||||||
} else protect(getInt.invoke(fd) as Int)
|
|
||||||
}) 0 else 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class NullConnectionException : NullPointerException(), BaseService.ExpectedException {
|
|
||||||
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val data = BaseService.Data(this)
|
|
||||||
override val tag: String get() = "ShadowsocksVpnService"
|
|
||||||
override fun createNotification(profileName: String): ServiceNotification =
|
|
||||||
ServiceNotification(this, profileName, "service-vpn")
|
|
||||||
|
|
||||||
private var conn: ParcelFileDescriptor? = null
|
|
||||||
private var worker: ProtectWorker? = null
|
|
||||||
private var active = false
|
|
||||||
|
|
||||||
// metered = false. xinlake
|
|
||||||
private var underlyingNetwork: Network? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (active) setUnderlyingNetworks(underlyingNetworks)
|
|
||||||
}
|
|
||||||
private val underlyingNetworks
|
|
||||||
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered
|
|
||||||
underlyingNetwork?.let { arrayOf(it) }
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent) = when (intent.action) {
|
|
||||||
SERVICE_INTERFACE -> super<VpnService>.onBind(intent)
|
|
||||||
else -> super<LocalDnsService.Interface>.onBind(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRevoke() = stopRunner()
|
|
||||||
|
|
||||||
override fun killProcesses(scope: CoroutineScope) {
|
|
||||||
super.killProcesses(scope)
|
|
||||||
active = false
|
|
||||||
scope.launch { DefaultNetworkListener.stop(this) }
|
|
||||||
worker?.shutdown(scope)
|
|
||||||
worker = null
|
|
||||||
conn?.close()
|
|
||||||
conn = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (prepare(this) != null) {
|
|
||||||
// startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
|
||||||
|
|
||||||
stopRunner()
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
|
|
||||||
override suspend fun resolver(host: String) =
|
|
||||||
DnsResolverCompat.resolve(DefaultNetworkListener.get(), host)
|
|
||||||
|
|
||||||
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
|
|
||||||
|
|
||||||
override suspend fun startProcesses(hosts: HostsFile) {
|
|
||||||
worker = ProtectWorker().apply { start() }
|
|
||||||
super.startProcesses(hosts)
|
|
||||||
sendFd(startVpn())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
|
|
||||||
cmd += "-V"
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startVpn(): FileDescriptor {
|
|
||||||
val profile = data.proxy!!.profile
|
|
||||||
val builder = Builder()
|
|
||||||
.setConfigureIntent(Core.configureIntent(this))
|
|
||||||
.setSession(profile.formattedName)
|
|
||||||
.setMtu(VPN_MTU)
|
|
||||||
.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
|
||||||
.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
|
||||||
|
|
||||||
if (profile.ipv6) {
|
|
||||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
|
||||||
builder.addRoute("::", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// XinLake. bypass lan
|
|
||||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
|
||||||
val subnet = Subnet.fromString(it)!!
|
|
||||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
|
||||||
}
|
|
||||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
|
||||||
|
|
||||||
active = true // possible race condition here?
|
|
||||||
if (Build.VERSION.SDK_INT >= 22) {
|
|
||||||
builder.setUnderlyingNetworks(underlyingNetworks)
|
|
||||||
}
|
|
||||||
|
|
||||||
val conn = builder.establish() ?: throw NullConnectionException()
|
|
||||||
this.conn = conn
|
|
||||||
|
|
||||||
val cmd = arrayListOf(
|
|
||||||
File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
|
|
||||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
|
||||||
"--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}",
|
|
||||||
"--tunmtu", VPN_MTU.toString(),
|
|
||||||
"--sock-path", "sock_path",
|
|
||||||
"--dnsgw", "127.0.0.1:${DataStore.portLocalDns}",
|
|
||||||
"--loglevel", "warning"
|
|
||||||
)
|
|
||||||
if (profile.ipv6) {
|
|
||||||
cmd += "--netif-ip6addr"
|
|
||||||
cmd += PRIVATE_VLAN6_ROUTER
|
|
||||||
}
|
|
||||||
cmd += "--enable-udprelay"
|
|
||||||
data.processes!!.start(cmd, onRestartCallback = {
|
|
||||||
try {
|
|
||||||
sendFd(conn.fileDescriptor)
|
|
||||||
} catch (e: ErrnoException) {
|
|
||||||
stopRunner(false, e.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return conn.fileDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendFd(fd: FileDescriptor) {
|
|
||||||
var tries = 0
|
|
||||||
val path = File(Core.deviceStorage.noBackupFilesDir, "sock_path").absolutePath
|
|
||||||
while (true) try {
|
|
||||||
delay(50L shl tries)
|
|
||||||
LocalSocket().use { localSocket ->
|
|
||||||
localSocket.connect(
|
|
||||||
LocalSocketAddress(
|
|
||||||
path,
|
|
||||||
LocalSocketAddress.Namespace.FILESYSTEM
|
|
||||||
)
|
|
||||||
)
|
|
||||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
|
||||||
localSocket.outputStream.write(42)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (tries > 5) throw e
|
|
||||||
tries += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
data.binder.close()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
*******************************************************************************/
|
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.net.DnsResolver
|
|
||||||
import android.net.Network
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import com.github.shadowsocks.Core
|
|
||||||
import com.github.shadowsocks.Core.app
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
sealed class DnsResolverCompat {
|
|
||||||
companion object : DnsResolverCompat() {
|
|
||||||
private val instance by lazy { if (Build.VERSION.SDK_INT >= 29) DnsResolverCompat29 else DnsResolverCompat21 }
|
|
||||||
|
|
||||||
override suspend fun resolve(network: Network, host: String) =
|
|
||||||
instance.resolve(network, host)
|
|
||||||
|
|
||||||
override suspend fun resolveOnActiveNetwork(host: String) =
|
|
||||||
instance.resolveOnActiveNetwork(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract suspend fun resolve(network: Network, host: String): Array<InetAddress>
|
|
||||||
abstract suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress>
|
|
||||||
|
|
||||||
private object DnsResolverCompat21 : DnsResolverCompat() {
|
|
||||||
/**
|
|
||||||
* This dispatcher is used for noncancellable possibly-forever-blocking operations in network IO.
|
|
||||||
*
|
|
||||||
* See also: https://issuetracker.google.com/issues/133874590
|
|
||||||
*/
|
|
||||||
private val unboundedIO by lazy {
|
|
||||||
if (app.getSystemService<ActivityManager>()!!.isLowRamDevice) Dispatchers.IO
|
|
||||||
else Executors.newCachedThreadPool().asCoroutineDispatcher()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun resolve(network: Network, host: String) =
|
|
||||||
GlobalScope.async(unboundedIO) { network.getAllByName(host) }.await()
|
|
||||||
|
|
||||||
override suspend fun resolveOnActiveNetwork(host: String) =
|
|
||||||
GlobalScope.async(unboundedIO) { InetAddress.getAllByName(host) }.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(29)
|
|
||||||
private object DnsResolverCompat29 : DnsResolverCompat(), Executor {
|
|
||||||
/**
|
|
||||||
* This executor will run on its caller directly. On Q beta 3 thru 4, this results in calling in main thread.
|
|
||||||
*/
|
|
||||||
override fun execute(command: Runnable) = command.run()
|
|
||||||
|
|
||||||
override suspend fun resolve(network: Network, host: String): Array<InetAddress> {
|
|
||||||
return suspendCancellableCoroutine { cont ->
|
|
||||||
val signal = CancellationSignal()
|
|
||||||
cont.invokeOnCancellation { signal.cancel() }
|
|
||||||
// retry should be handled by client instead
|
|
||||||
DnsResolver.getInstance().query(network, host, DnsResolver.FLAG_NO_RETRY, this, signal,
|
|
||||||
object : DnsResolver.Callback<Collection<InetAddress>> {
|
|
||||||
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) =
|
|
||||||
cont.resume(answer.toTypedArray())
|
|
||||||
|
|
||||||
override fun onError(error: DnsResolver.DnsException) =
|
|
||||||
cont.resumeWithException(IOException(error))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress> {
|
|
||||||
return resolve(Core.connectivity.activeNetwork ?: return emptyArray(), host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
*******************************************************************************/
|
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.github.shadowsocks.Core
|
|
||||||
import com.github.shadowsocks.database.Profile
|
|
||||||
import com.github.shadowsocks.net.HostsFile
|
|
||||||
import com.github.shadowsocks.preference.DataStore
|
|
||||||
import com.github.shadowsocks.utils.parseNumericAddress
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class sets up environment for ss-local.
|
|
||||||
*/
|
|
||||||
class ProxyInstance(val profile: Profile) {
|
|
||||||
private var configFile: File? = null
|
|
||||||
var trafficMonitor: TrafficMonitor? = null
|
|
||||||
|
|
||||||
fun getFile(context: Context = Core.deviceStorage) =
|
|
||||||
File(context.noBackupFilesDir, "bypass-lan.acl")
|
|
||||||
|
|
||||||
suspend fun init(service: BaseService.Interface, hosts: HostsFile) {
|
|
||||||
// it's hard to resolve DNS on a specific interface so we'll do it here
|
|
||||||
if (profile.host.parseNumericAddress() == null) {
|
|
||||||
profile.host = (hosts.resolve(profile.host).firstOrNull() ?: try {
|
|
||||||
service.resolver(profile.host).firstOrNull()
|
|
||||||
} catch (_: IOException) {
|
|
||||||
null
|
|
||||||
})?.hostAddress ?: throw UnknownHostException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
|
|
||||||
* device storage, depending on which is currently available.
|
|
||||||
*/
|
|
||||||
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
|
|
||||||
trafficMonitor = TrafficMonitor(stat)
|
|
||||||
|
|
||||||
this.configFile = configFile
|
|
||||||
val config = profile.toJson()
|
|
||||||
configFile.writeText(config.toString())
|
|
||||||
|
|
||||||
val cmd = service.buildAdditionalArguments(arrayListOf(
|
|
||||||
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
|
||||||
"-b", DataStore.listenAddress,
|
|
||||||
"-l", DataStore.portProxy.toString(),
|
|
||||||
"-t", "600",
|
|
||||||
"-S", stat.absolutePath,
|
|
||||||
"-c", configFile.absolutePath))
|
|
||||||
if (extraFlag != null) cmd.add(extraFlag)
|
|
||||||
|
|
||||||
cmd += "--acl"
|
|
||||||
cmd += getFile().absolutePath
|
|
||||||
|
|
||||||
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
|
|
||||||
cmd += "-D"
|
|
||||||
|
|
||||||
if (DataStore.tcpFastOpen) cmd += "--fast-open"
|
|
||||||
|
|
||||||
service.data.processes!!.start(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shutdown(scope: CoroutineScope) {
|
|
||||||
trafficMonitor?.apply {
|
|
||||||
thread.shutdown(scope)
|
|
||||||
persistStats(profile.id) // Make sure update total traffic when stopping the runner
|
|
||||||
}
|
|
||||||
trafficMonitor = null
|
|
||||||
configFile?.delete() // remove old config possibly in device storage
|
|
||||||
configFile = null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
*******************************************************************************/
|
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.PowerManager
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import com.github.shadowsocks.Core
|
|
||||||
import org.amnezia.vpn.R
|
|
||||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
|
||||||
import com.github.shadowsocks.aidl.TrafficStats
|
|
||||||
import com.github.shadowsocks.utils.Action
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User can customize visibility of notification since Android 8.
|
|
||||||
* The default visibility:
|
|
||||||
*
|
|
||||||
* Android 8.x: always visible due to system limitations
|
|
||||||
* VPN: always invisible because of VPN notification/icon
|
|
||||||
* Other: always visible
|
|
||||||
*
|
|
||||||
* See also: https://github.com/aosp-mirror/platform_frameworks_base/commit/070d142993403cc2c42eca808ff3fafcee220ac4
|
|
||||||
*/
|
|
||||||
class ServiceNotification(private val service: BaseService.Interface, profileName: String, channel: String, visible: Boolean = false)
|
|
||||||
: BroadcastReceiver() {
|
|
||||||
private val callback: IShadowsocksServiceCallback by lazy {
|
|
||||||
object : IShadowsocksServiceCallback.Stub() {
|
|
||||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {} // ignore
|
|
||||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
|
||||||
if (profileId != 0L) return
|
|
||||||
builder.apply {
|
|
||||||
setContentText((service as Context).getString(R.string.traffic,
|
|
||||||
service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate)),
|
|
||||||
service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))))
|
|
||||||
setSubText(service.getString(R.string.traffic,
|
|
||||||
Formatter.formatFileSize(service, stats.txTotal),
|
|
||||||
Formatter.formatFileSize(service, stats.rxTotal)))
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun trafficPersisted(profileId: Long) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var callbackRegistered = false
|
|
||||||
|
|
||||||
private val builder = NotificationCompat.Builder(service as Context, channel)
|
|
||||||
.setWhen(0)
|
|
||||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
|
||||||
.setTicker(service.getString(R.string.forward_success))
|
|
||||||
.setContentTitle(profileName)
|
|
||||||
.setContentIntent(Core.configureIntent(service))
|
|
||||||
.setSmallIcon(R.drawable.ic_service_active)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
||||||
.setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)
|
|
||||||
|
|
||||||
init {
|
|
||||||
service as Context
|
|
||||||
val closeAction = NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_navigation_close,
|
|
||||||
service.getString(R.string.stop),
|
|
||||||
PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0)).apply {
|
|
||||||
setShowsUserInterface(false)
|
|
||||||
}.build()
|
|
||||||
if (Build.VERSION.SDK_INT < 24) builder.addAction(closeAction) else builder.addInvisibleAction(closeAction)
|
|
||||||
updateCallback(service.getSystemService<PowerManager>()?.isInteractive != false)
|
|
||||||
service.registerReceiver(this, IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_SCREEN_ON)
|
|
||||||
addAction(Intent.ACTION_SCREEN_OFF)
|
|
||||||
})
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (service.data.state == BaseService.State.Connected) updateCallback(intent.action == Intent.ACTION_SCREEN_ON)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCallback(screenOn: Boolean) {
|
|
||||||
if (screenOn) {
|
|
||||||
service.data.binder.registerCallback(callback)
|
|
||||||
service.data.binder.startListeningForBandwidth(callback, 1000)
|
|
||||||
callbackRegistered = true
|
|
||||||
} else if (callbackRegistered) { // unregister callback to save battery
|
|
||||||
service.data.binder.unregisterCallback(callback)
|
|
||||||
callbackRegistered = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun show() = (service as Service).startForeground(1, builder.build())
|
|
||||||
|
|
||||||
fun destroy() {
|
|
||||||
(service as Service).unregisterReceiver(this)
|
|
||||||
updateCallback(false)
|
|
||||||
service.stopForeground(true)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
*******************************************************************************/
|
|
||||||
|
|
||||||
package com.github.shadowsocks.database
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.LongSparseArray
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.room.*
|
|
||||||
import com.github.shadowsocks.preference.DataStore
|
|
||||||
import com.github.shadowsocks.utils.Key
|
|
||||||
import com.github.shadowsocks.utils.parsePort
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.Serializable
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URISyntaxException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Parcelize
|
|
||||||
data class Profile(
|
|
||||||
// XinLake. route mode is bypass-lan
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
var id: Long = 0,
|
|
||||||
var name: String? = "",
|
|
||||||
var host: String = "0.0.0.0",
|
|
||||||
var remotePort: Int = 0,
|
|
||||||
var password: String = "0000",
|
|
||||||
var method: String = "aes-256-cfb",
|
|
||||||
var remoteDns: String = "8.8.8.8",
|
|
||||||
var udpdns: Boolean = false,
|
|
||||||
var ipv6: Boolean = false,
|
|
||||||
//@TargetApi(28)
|
|
||||||
var tx: Long = 0,
|
|
||||||
var rx: Long = 0,
|
|
||||||
var userOrder: Long = 0,
|
|
||||||
|
|
||||||
@Ignore // not persisted in db, only used by direct boot
|
|
||||||
var dirty: Boolean = false) : Parcelable, Serializable {
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "ShadowParser"
|
|
||||||
private const val serialVersionUID = 1L
|
|
||||||
private val pattern =
|
|
||||||
"""(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
|
|
||||||
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
|
|
||||||
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
|
|
||||||
|
|
||||||
fun findAllUrls(data: CharSequence?, feature: Profile? = null) =
|
|
||||||
pattern.findAll(data ?: "").map {
|
|
||||||
val uri = it.value.toUri()
|
|
||||||
try {
|
|
||||||
if (uri.userInfo == null) {
|
|
||||||
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
|
|
||||||
if (match != null) {
|
|
||||||
val profile = Profile()
|
|
||||||
feature?.copyFeatureSettingsTo(profile)
|
|
||||||
profile.method = match.groupValues[1].toLowerCase(Locale.ENGLISH)
|
|
||||||
profile.password = match.groupValues[2]
|
|
||||||
profile.host = match.groupValues[3]
|
|
||||||
profile.remotePort = match.groupValues[4].toInt()
|
|
||||||
profile.name = uri.fragment
|
|
||||||
profile
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Unrecognized URI: ${it.value}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
|
|
||||||
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
|
|
||||||
if (match != null) {
|
|
||||||
val profile = Profile()
|
|
||||||
feature?.copyFeatureSettingsTo(profile)
|
|
||||||
profile.method = match.groupValues[1]
|
|
||||||
profile.password = match.groupValues[2]
|
|
||||||
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
|
|
||||||
try {
|
|
||||||
val javaURI = URI(it.value)
|
|
||||||
profile.host = javaURI.host ?: ""
|
|
||||||
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']') {
|
|
||||||
profile.host = profile.host.substring(1, profile.host.length - 1)
|
|
||||||
}
|
|
||||||
profile.remotePort = javaURI.port
|
|
||||||
profile.name = uri.fragment ?: ""
|
|
||||||
profile
|
|
||||||
} catch (e: URISyntaxException) {
|
|
||||||
Log.e(TAG, "Invalid URI: ${it.value}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Unknown user info: ${it.value}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Log.e(TAG, "Invalid base64 detected: ${it.value}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
private class JsonParser(private val feature: Profile? = null) : ArrayList<Profile>() {
|
|
||||||
private val JsonElement?.optString get() = (this as? JsonPrimitive)?.asString
|
|
||||||
private val JsonElement?.optBoolean
|
|
||||||
get() = // asBoolean attempts to cast everything to boolean
|
|
||||||
(this as? JsonPrimitive)?.run { if (isBoolean) asBoolean else null }
|
|
||||||
private val JsonElement?.optInt
|
|
||||||
get() = try {
|
|
||||||
(this as? JsonPrimitive)?.asInt
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryParse(json: JsonObject, fallback: Boolean = false): Profile? {
|
|
||||||
val host = json["server"].optString
|
|
||||||
if (host.isNullOrEmpty()) return null
|
|
||||||
val remotePort = json["server_port"]?.optInt
|
|
||||||
if (remotePort == null || remotePort <= 0) return null
|
|
||||||
val password = json["password"].optString
|
|
||||||
if (password.isNullOrEmpty()) return null
|
|
||||||
val method = json["method"].optString
|
|
||||||
if (method.isNullOrEmpty()) return null
|
|
||||||
return Profile().also {
|
|
||||||
it.host = host
|
|
||||||
it.remotePort = remotePort
|
|
||||||
it.password = password
|
|
||||||
it.method = method
|
|
||||||
}.apply {
|
|
||||||
feature?.copyFeatureSettingsTo(this)
|
|
||||||
name = json["remarks"].optString
|
|
||||||
if (fallback) return@apply
|
|
||||||
remoteDns = json["remote_dns"].optString ?: remoteDns
|
|
||||||
ipv6 = json["ipv6"].optBoolean ?: ipv6
|
|
||||||
udpdns = json["udpdns"].optBoolean ?: udpdns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun process(json: JsonElement?) {
|
|
||||||
when (json) {
|
|
||||||
is JsonObject -> {
|
|
||||||
val profile = tryParse(json)
|
|
||||||
if (profile != null) add(profile) else for ((_, value) in json.entrySet()) process(value)
|
|
||||||
}
|
|
||||||
is JsonArray -> json.asIterable().forEach(this::process)
|
|
||||||
// ignore other types
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseJson(json: JsonElement, feature: Profile? = null, create: (Profile) -> Unit) {
|
|
||||||
JsonParser(feature).run {
|
|
||||||
process(json)
|
|
||||||
for (profile in this) create(profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@androidx.room.Dao
|
|
||||||
interface Dao {
|
|
||||||
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
|
|
||||||
operator fun get(id: Long): Profile?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
|
|
||||||
fun list(): List<Profile>
|
|
||||||
|
|
||||||
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
|
|
||||||
fun nextOrder(): Long?
|
|
||||||
|
|
||||||
@Query("SELECT 1 FROM `Profile` LIMIT 1")
|
|
||||||
fun isNotEmpty(): Boolean
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
fun create(value: Profile): Long
|
|
||||||
|
|
||||||
@Update
|
|
||||||
fun update(value: Profile): Int
|
|
||||||
|
|
||||||
@Query("DELETE FROM `Profile` WHERE `id` = :id")
|
|
||||||
fun delete(id: Long): Int
|
|
||||||
|
|
||||||
@Query("DELETE FROM `Profile`")
|
|
||||||
fun deleteAll(): Int
|
|
||||||
}
|
|
||||||
|
|
||||||
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
|
|
||||||
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
|
|
||||||
|
|
||||||
fun copyFeatureSettingsTo(profile: Profile) {
|
|
||||||
profile.ipv6 = ipv6
|
|
||||||
profile.udpdns = udpdns
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(): Uri {
|
|
||||||
val auth = Base64.encodeToString("$method:$password".toByteArray(),
|
|
||||||
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
|
|
||||||
val wrappedHost = if (host.contains(':')) "[$host]" else host
|
|
||||||
val builder = Uri.Builder().scheme("ss").encodedAuthority("$auth@$wrappedHost:$remotePort")
|
|
||||||
if (!name.isNullOrEmpty()) builder.fragment(name)
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = toUri().toString()
|
|
||||||
|
|
||||||
fun toJson(profiles: LongSparseArray<Profile>? = null): JSONObject = JSONObject().apply {
|
|
||||||
put("server", host)
|
|
||||||
put("server_port", remotePort)
|
|
||||||
put("password", password)
|
|
||||||
put("method", method)
|
|
||||||
if (profiles == null) return@apply
|
|
||||||
put("remarks", name)
|
|
||||||
put("remote_dns", remoteDns)
|
|
||||||
put("ipv6", ipv6)
|
|
||||||
put("udpdns", udpdns)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun serialize() {
|
|
||||||
DataStore.editingId = id
|
|
||||||
DataStore.privateStore.putString(Key.name, name)
|
|
||||||
DataStore.privateStore.putString(Key.host, host)
|
|
||||||
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
|
|
||||||
DataStore.privateStore.putString(Key.password, password)
|
|
||||||
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
|
|
||||||
DataStore.privateStore.putString(Key.method, method)
|
|
||||||
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
|
|
||||||
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
|
|
||||||
DataStore.privateStore.remove(Key.dirty)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deserialize() {
|
|
||||||
check(id == 0L || DataStore.editingId == id)
|
|
||||||
DataStore.editingId = null
|
|
||||||
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
|
|
||||||
name = DataStore.privateStore.getString(Key.name) ?: ""
|
|
||||||
// It's safe to trim the hostname, as we expect no leading or trailing whitespaces here
|
|
||||||
host = (DataStore.privateStore.getString(Key.host) ?: "").trim()
|
|
||||||
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
|
|
||||||
password = DataStore.privateStore.getString(Key.password) ?: ""
|
|
||||||
method = DataStore.privateStore.getString(Key.method) ?: ""
|
|
||||||
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
|
|
||||||
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
|
|
||||||
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,7 +39,6 @@ class VPNService : android.net.VpnService() {
|
||||||
Log.e(tag, "Wireguard Version ${wgVersion()}")
|
Log.e(tag, "Wireguard Version ${wgVersion()}")
|
||||||
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
|
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
|
||||||
mAlreadyInitialised = true
|
mAlreadyInitialised = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnbind(intent: Intent?): Boolean {
|
override fun onUnbind(intent: Intent?): Boolean {
|
||||||
|
@ -82,10 +81,7 @@ class VPNService : android.net.VpnService() {
|
||||||
val lastConfString = prefs.getString("lastConf", "")
|
val lastConfString = prefs.getString("lastConf", "")
|
||||||
if (lastConfString.isNullOrEmpty()) {
|
if (lastConfString.isNullOrEmpty()) {
|
||||||
// We have nothing to connect to -> Exit
|
// We have nothing to connect to -> Exit
|
||||||
Log.e(
|
Log.e(tag,"VPN service was triggered without defining a Server or having a tunnel")
|
||||||
tag,
|
|
||||||
"VPN service was triggered without defining a Server or having a tunnel"
|
|
||||||
)
|
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
this.mConfig = JSONObject(lastConfString)
|
this.mConfig = JSONObject(lastConfString)
|
||||||
|
@ -156,10 +152,13 @@ class VPNService : android.net.VpnService() {
|
||||||
}
|
}
|
||||||
Log.i(tag, "Permission okay")
|
Log.i(tag, "Permission okay")
|
||||||
mConfig = json!!
|
mConfig = json!!
|
||||||
|
Log.i(tag, "Config: " + mConfig)
|
||||||
mProtocol = mConfig!!.getString("protocol")
|
mProtocol = mConfig!!.getString("protocol")
|
||||||
|
Log.i(tag, "Protocol: " + mProtocol)
|
||||||
when (mProtocol) {
|
when (mProtocol) {
|
||||||
"openvpn" -> startOpenVpn()
|
"openvpn" -> startOpenVpn()
|
||||||
"wireguard" -> startWireGuard()
|
"wireguard" -> startWireGuard()
|
||||||
|
"shadowsocks" -> startShadowsocks()
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(tag, "No protocol")
|
Log.e(tag, "No protocol")
|
||||||
return 0
|
return 0
|
||||||
|
@ -365,6 +364,19 @@ class VPNService : android.net.VpnService() {
|
||||||
return mConfig!!
|
return mConfig!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startShadowsocks() {
|
||||||
|
Log.e(tag, "startShadowsocks method enters")
|
||||||
|
if(mConfig != null) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(tag, "Error in startShadowsocks: $e")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(tag, "Invalid config file!!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startOpenVpn() {
|
private fun startOpenVpn() {
|
||||||
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
|
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
|
||||||
Thread({
|
Thread({
|
||||||
|
|
|
@ -54,7 +54,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
||||||
val json = buffer?.let { String(it) }
|
val json = buffer?.let { String(it) }
|
||||||
val config = JSONObject(json)
|
val config = JSONObject(json)
|
||||||
Log.v(tag, "Stored new Tunnel config in Service")
|
Log.v(tag, "Stored new Tunnel config in Service")
|
||||||
|
Log.i(tag, "Config: $config")
|
||||||
if (!mService.checkPermissions()) {
|
if (!mService.checkPermissions()) {
|
||||||
mResumeConfig = config
|
mResumeConfig = config
|
||||||
// The Permission prompt was already
|
// The Permission prompt was already
|
||||||
|
|
22
client/android/src/org/amnezia/vpn/qt/AmneziaApp.kt
Normal file
22
client/android/src/org/amnezia/vpn/qt/AmneziaApp.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package org.amnezia.vpn.qt
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.VpnManager
|
||||||
|
import org.qtproject.qt5.android.bindings.QtActivity
|
||||||
|
import org.qtproject.qt5.android.bindings.QtApplication
|
||||||
|
import android.app.Application
|
||||||
|
|
||||||
|
class AmneziaApp: Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Core.init(this, QtActivity::class)
|
||||||
|
VpnManager.getInstance().init(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
Core.updateNotificationChannels()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,24 @@
|
||||||
package org.amnezia.vpn.qt;
|
package org.amnezia.vpn.qt;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import androidx.annotation.NonNull;
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core;
|
||||||
import org.amnezia.vpn.BuildConfig;
|
import org.amnezia.vpn.shadowsocks.core.VpnManager;
|
||||||
|
|
||||||
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
|
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
|
||||||
|
|
||||||
private static VPNApplication instance;
|
private static VPNApplication instance;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
VPNApplication.instance = this;
|
VPNApplication.instance = this;
|
||||||
|
// Core.INSTANCE.init(this, VPNActivity.class);
|
||||||
|
// VpnManager.Companion.getInstance().init(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
// Core.INSTANCE.updateNotificationChannels();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
companion object {
|
||||||
|
private val componentName by lazy { ComponentName(app, org.amnezia.vpn.shadowsocks.core.BootReceiver::class.java) }
|
||||||
|
var enabled: Boolean
|
||||||
|
get() = app.packageManager.getComponentEnabledSetting(org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName) ==
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
|
set(value) = app.packageManager.setComponentEnabledSetting(
|
||||||
|
org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName,
|
||||||
|
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
|
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val locked = when (intent.action) {
|
||||||
|
Intent.ACTION_BOOT_COMPLETED -> false
|
||||||
|
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
if (DataStore.directBootAware == locked) org.amnezia.vpn.shadowsocks.core.Core.startService()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks
|
package org.amnezia.vpn.shadowsocks.core
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
|
@ -31,7 +31,6 @@ import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
@ -39,17 +38,17 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.github.shadowsocks.aidl.ShadowsocksConnection
|
|
||||||
import com.github.shadowsocks.database.Profile
|
import org.amnezia.vpn.R
|
||||||
import com.github.shadowsocks.database.ProfileManager
|
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||||
import com.github.shadowsocks.net.TcpFastOpen
|
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||||
import com.github.shadowsocks.preference.DataStore
|
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||||
import com.github.shadowsocks.utils.*
|
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.*
|
||||||
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
||||||
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.amnezia.vpn.R
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
@ -59,16 +58,16 @@ object Core {
|
||||||
|
|
||||||
lateinit var app: Application
|
lateinit var app: Application
|
||||||
lateinit var configureIntent: (Context) -> PendingIntent
|
lateinit var configureIntent: (Context) -> PendingIntent
|
||||||
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
|
|
||||||
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
|
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
|
||||||
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
|
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
|
||||||
val directBootSupported by lazy {
|
val directBootSupported by lazy {
|
||||||
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
|
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
|
||||||
|
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeProfileIds
|
val activeProfileIds
|
||||||
get() = ProfileManager.getProfile(DataStore.profileId).let {
|
get() = ProfileManager.getProfile(DataStore.profileId).let {
|
||||||
if (it == null) emptyList() else listOfNotNull(it.id)
|
if (it == null) emptyList() else listOfNotNull(it.id, it.udpFallback)
|
||||||
}
|
}
|
||||||
val currentProfile: Pair<Profile, Profile?>?
|
val currentProfile: Pair<Profile, Profile?>?
|
||||||
get() {
|
get() {
|
||||||
|
@ -84,37 +83,34 @@ object Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(app: Application, configureClass: KClass<out Any>) {
|
fun init(app: Application, configureClass: KClass<out Any>) {
|
||||||
this.app = app
|
Core.app = app
|
||||||
this.configureIntent = {
|
configureIntent = {
|
||||||
PendingIntent.getActivity(it,
|
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
|
||||||
0,
|
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
|
||||||
Intent(it,
|
|
||||||
configureClass.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
|
||||||
0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||||
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
|
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
|
||||||
|
val old = Acl.getFile(Acl.CUSTOM_RULES, app)
|
||||||
|
if (old.canRead()) {
|
||||||
|
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
||||||
|
old.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
||||||
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
||||||
// Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
|
WorkManager.initialize(deviceStorage, Configuration.Builder().build())
|
||||||
// FirebaseApp.initializeApp(deviceStorage)
|
|
||||||
WorkManager.initialize(deviceStorage, Configuration.Builder().apply {
|
|
||||||
setExecutor { GlobalScope.launch { it.run() } }
|
|
||||||
setTaskExecutor { GlobalScope.launch { it.run() } }
|
|
||||||
}.build())
|
|
||||||
|
|
||||||
// handle data restored/crash
|
// handle data restored/crash
|
||||||
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware && app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
|
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
|
||||||
|
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
|
||||||
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
|
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
|
||||||
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
|
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
|
||||||
val assetManager = app.assets
|
val assetManager = app.assets
|
||||||
try {
|
try {
|
||||||
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
|
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
|
||||||
File(deviceStorage.noBackupFilesDir, file).outputStream()
|
File(ContextCompat.getNoBackupFilesDir(deviceStorage), file).outputStream().use { output -> input.copyTo(output) }
|
||||||
.use { output -> input.copyTo(output) }
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
printLog(e)
|
printLog(e)
|
||||||
|
@ -127,11 +123,13 @@ object Core {
|
||||||
fun updateNotificationChannels() {
|
fun updateNotificationChannels() {
|
||||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||||
val nm = app.getSystemService<NotificationManager>()!!
|
val nm = app.getSystemService<NotificationManager>()!!
|
||||||
nm.createNotificationChannels(listOf(NotificationChannel("service-vpn",
|
nm.createNotificationChannels(listOf(
|
||||||
app.getText(R.string.service_vpn),
|
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
|
||||||
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
else NotificationManager.IMPORTANCE_LOW) // #1355
|
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
|
||||||
))
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
|
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
|
||||||
|
NotificationManager.IMPORTANCE_LOW)))
|
||||||
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,14 +138,11 @@ object Core {
|
||||||
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
||||||
|
|
||||||
fun startService() =
|
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||||
ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
|
||||||
|
|
||||||
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
|
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
|
||||||
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
|
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
|
||||||
|
|
||||||
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() {
|
||||||
object : BroadcastReceiver() {
|
|
||||||
init {
|
init {
|
||||||
app.registerReceiver(this, IntentFilter().apply {
|
app.registerReceiver(this, IntentFilter().apply {
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
@ -0,0 +1,170 @@
|
||||||
|
package org.amnezia.vpn.shadowsocks.core
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.os.DeadObjectException
|
||||||
|
import android.os.Handler
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author : kyle
|
||||||
|
* e-mail : 1239878682@qq.com
|
||||||
|
* @date : 2019/5/14 16:54
|
||||||
|
* 看了我的代码,感动了吗?
|
||||||
|
*/
|
||||||
|
class VpnManager private constructor() {
|
||||||
|
|
||||||
|
var state = BaseService.State.Idle
|
||||||
|
private var context: Context? = null
|
||||||
|
private val handler = Handler()
|
||||||
|
private val connection = ShadowsocksConnection(handler, true)
|
||||||
|
private var listener: OnStatusChangeListener? = null
|
||||||
|
private val callback: ShadowsocksConnection.Callback = object : ShadowsocksConnection.Callback {
|
||||||
|
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {
|
||||||
|
changeState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
|
||||||
|
|
||||||
|
override fun onServiceConnected(service: IShadowsocksService) {
|
||||||
|
changeState(try {
|
||||||
|
BaseService.State.values()[service.state]
|
||||||
|
} catch (_: DeadObjectException) {
|
||||||
|
BaseService.State.Idle
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||||
|
super.trafficUpdated(profileId, stats)
|
||||||
|
listener?.onTrafficUpdated(profileId, stats)
|
||||||
|
}
|
||||||
|
override fun onBinderDied() {
|
||||||
|
disconnect()
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connect() {
|
||||||
|
context?.let {
|
||||||
|
connection.connect(it, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnect() {
|
||||||
|
context?.let { connection.disconnect(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REQUEST_CONNECT = 1
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private var instance: VpnManager? = null
|
||||||
|
|
||||||
|
fun getInstance(): VpnManager {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = VpnManager()
|
||||||
|
}
|
||||||
|
return instance as VpnManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(context: Context){
|
||||||
|
this.context=context
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 开启或者关闭 自动判断
|
||||||
|
*/
|
||||||
|
fun run(activity:Activity) {
|
||||||
|
when {
|
||||||
|
state.canStop -> Core.stopService()
|
||||||
|
DataStore.serviceMode == Key.modeVpn -> {
|
||||||
|
val intent = VpnService.prepare(activity)
|
||||||
|
if (intent != null) activity.startActivityForResult(intent, REQUEST_CONNECT)
|
||||||
|
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
|
||||||
|
}
|
||||||
|
else -> Core.startService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 设置状态监听
|
||||||
|
*/
|
||||||
|
fun setOnStatusChangeListener(listener: OnStatusChangeListener) {
|
||||||
|
this.listener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* application调用stop时调用
|
||||||
|
*/
|
||||||
|
fun onStop() {
|
||||||
|
connection.bandwidthTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStart() {
|
||||||
|
connection.bandwidthTimeout = 1000
|
||||||
|
}
|
||||||
|
/***
|
||||||
|
* activity调用onActivityResult时调用
|
||||||
|
*/
|
||||||
|
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
when {
|
||||||
|
requestCode != REQUEST_CONNECT -> {
|
||||||
|
}
|
||||||
|
resultCode == Activity.RESULT_OK -> Core.startService()
|
||||||
|
else -> {
|
||||||
|
//无权限
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 改变当前状态
|
||||||
|
*/
|
||||||
|
private fun changeState(state: BaseService.State) {
|
||||||
|
this.state = state
|
||||||
|
this.listener?.onStatusChanged(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 状态改变监听器
|
||||||
|
*/
|
||||||
|
interface OnStatusChangeListener {
|
||||||
|
fun onStatusChanged(state: BaseService.State)
|
||||||
|
|
||||||
|
fun onTrafficUpdated(profileId: Long, stats: TrafficStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Route(name: String) {
|
||||||
|
//全部
|
||||||
|
ALL("all")
|
||||||
|
//绕过局域网地址
|
||||||
|
,
|
||||||
|
BY_PASS_LAN("bypass-lan")
|
||||||
|
//绕过中国大陆地址
|
||||||
|
,
|
||||||
|
BY_PASS_CHINA("bypass-china")
|
||||||
|
//绕过局域网和中国大陆地址
|
||||||
|
,
|
||||||
|
BY_PASS_LAN_CHINA("bypass-lan-china")
|
||||||
|
//GFW列表
|
||||||
|
,
|
||||||
|
GFW_LIST("gfwlist")
|
||||||
|
//仅代理中国大陆地址
|
||||||
|
,
|
||||||
|
CHINA_LIST("china-list")
|
||||||
|
//自定义规则
|
||||||
|
,
|
||||||
|
CUSTOM_RULES("custom-rules");
|
||||||
|
|
||||||
|
var route = name
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||||
|
|
||||||
|
class VpnRequestActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "VpnRequestActivity"
|
||||||
|
private const val REQUEST_CONNECT = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private var receiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (DataStore.serviceMode != Key.modeVpn) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
|
||||||
|
receiver = broadcastReceiver { _, _ -> request() }
|
||||||
|
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
|
||||||
|
} else request()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun request() {
|
||||||
|
val intent = VpnService.prepare(this)
|
||||||
|
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
|
||||||
|
else startActivityForResult(intent, REQUEST_CONNECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (resultCode == RESULT_OK) Core.startService() else {
|
||||||
|
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (receiver != null) unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
}
|
200
client/android/src/org/amnezia/vpn/shadowsocks/core/acl/Acl.kt
Normal file
200
client/android/src/org/amnezia/vpn/shadowsocks/core/acl/Acl.kt
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.acl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.recyclerview.widget.SortedList
|
||||||
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.Reader
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
class Acl {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "Acl"
|
||||||
|
const val ALL = "all"
|
||||||
|
const val BYPASS_LAN = "bypass-lan"
|
||||||
|
const val BYPASS_CHN = "bypass-china"
|
||||||
|
const val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||||
|
const val GFWLIST = "gfwlist"
|
||||||
|
const val CHINALIST = "china-list"
|
||||||
|
const val CUSTOM_RULES = "custom-rules"
|
||||||
|
|
||||||
|
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||||
|
|
||||||
|
fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.noBackupFilesDir, "$id.acl")
|
||||||
|
|
||||||
|
var customRules: Acl
|
||||||
|
get() {
|
||||||
|
val acl = Acl()
|
||||||
|
val str = DataStore.publicStore.getString(CUSTOM_RULES)
|
||||||
|
if (str != null) acl.fromReader(str.reader(), true)
|
||||||
|
if (!acl.bypass) {
|
||||||
|
acl.bypass = true
|
||||||
|
acl.subnets.clear()
|
||||||
|
}
|
||||||
|
return acl
|
||||||
|
}
|
||||||
|
set(value) = DataStore.publicStore.putString(CUSTOM_RULES,
|
||||||
|
if ((!value.bypass || value.subnets.size() == 0) && value.bypassHostnames.size() == 0 &&
|
||||||
|
value.proxyHostnames.size() == 0 && value.urls.size() == 0) null else value.toString())
|
||||||
|
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||||
|
override fun onInserted(position: Int, count: Int) { }
|
||||||
|
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) { }
|
||||||
|
override fun onChanged(position: Int, count: Int) { }
|
||||||
|
override fun onRemoved(position: Int, count: Int) { }
|
||||||
|
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
|
||||||
|
override fun compare(o1: T?, o2: T?): Int =
|
||||||
|
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
|
||||||
|
abstract fun compareNonNull(o1: T, o2: T): Int
|
||||||
|
}
|
||||||
|
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
|
||||||
|
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
|
||||||
|
}
|
||||||
|
private object StringSorter : DefaultSorter<String>()
|
||||||
|
private object SubnetSorter : DefaultSorter<Subnet>()
|
||||||
|
private object URLSorter : BaseSorter<URL>() {
|
||||||
|
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
|
||||||
|
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bypassHostnames = SortedList(String::class.java, StringSorter)
|
||||||
|
val proxyHostnames = SortedList(String::class.java, StringSorter)
|
||||||
|
val subnets = SortedList(Subnet::class.java, SubnetSorter)
|
||||||
|
val urls = SortedList(URL::class.java, URLSorter)
|
||||||
|
var bypass = false
|
||||||
|
|
||||||
|
fun fromAcl(other: Acl): Acl {
|
||||||
|
bypassHostnames.clear()
|
||||||
|
for (item in other.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||||
|
proxyHostnames.clear()
|
||||||
|
for (item in other.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||||
|
subnets.clear()
|
||||||
|
for (item in other.subnets.asIterable()) subnets.add(item)
|
||||||
|
urls.clear()
|
||||||
|
for (item in other.urls.asIterable()) urls.add(item)
|
||||||
|
bypass = other.bypass
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl {
|
||||||
|
bypassHostnames.clear()
|
||||||
|
proxyHostnames.clear()
|
||||||
|
subnets.clear()
|
||||||
|
urls.clear()
|
||||||
|
bypass = defaultBypass
|
||||||
|
val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||||
|
val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||||
|
var hostnames: SortedList<String>? = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||||
|
var subnets: SortedList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
|
||||||
|
reader.useLines {
|
||||||
|
for (line in it) {
|
||||||
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
|
val blocks = (line as java.lang.String).split("#", 2)
|
||||||
|
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
|
||||||
|
if (url != null) urls.add(URL(url))
|
||||||
|
when (val input = blocks[0].trim()) {
|
||||||
|
"[outbound_block_list]" -> {
|
||||||
|
hostnames = null
|
||||||
|
subnets = null
|
||||||
|
}
|
||||||
|
"[black_list]", "[bypass_list]" -> {
|
||||||
|
hostnames = bypassHostnames
|
||||||
|
subnets = bypassSubnets
|
||||||
|
}
|
||||||
|
"[white_list]", "[proxy_list]" -> {
|
||||||
|
hostnames = proxyHostnames
|
||||||
|
subnets = proxySubnets
|
||||||
|
}
|
||||||
|
"[reject_all]", "[bypass_all]" -> bypass = true
|
||||||
|
"[accept_all]", "[proxy_all]" -> bypass = false
|
||||||
|
else -> if (subnets != null && input.isNotEmpty()) {
|
||||||
|
val subnet = Subnet.fromString(input)
|
||||||
|
if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromId(id: String): Acl = try {
|
||||||
|
fromReader(getFile(id).bufferedReader())
|
||||||
|
} catch (_: IOException) { this }
|
||||||
|
|
||||||
|
suspend fun flatten(depth: Int, connect: suspend (URL) -> URLConnection): Acl {
|
||||||
|
if (depth > 0) for (url in urls.asIterable()) {
|
||||||
|
val child = Acl()
|
||||||
|
try {
|
||||||
|
child.fromReader(connect(url).getInputStream().bufferedReader(), bypass).flatten(depth - 1, connect)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (bypass != child.bypass) {
|
||||||
|
child.subnets.clear() // subnets for the different mode are discarded
|
||||||
|
child.bypass = bypass
|
||||||
|
}
|
||||||
|
for (item in child.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||||
|
for (item in child.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||||
|
for (item in child.subnets.asIterable()) subnets.add(item)
|
||||||
|
}
|
||||||
|
urls.clear()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val result = StringBuilder()
|
||||||
|
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||||
|
val bypassList = (if (bypass) {
|
||||||
|
bypassHostnames.asIterable().asSequence()
|
||||||
|
} else {
|
||||||
|
subnets.asIterable().asSequence().map(Subnet::toString) + bypassHostnames.asIterable().asSequence()
|
||||||
|
}).toList()
|
||||||
|
val proxyList = (if (bypass) {
|
||||||
|
subnets.asIterable().asSequence().map(Subnet::toString) + proxyHostnames.asIterable().asSequence()
|
||||||
|
} else {
|
||||||
|
proxyHostnames.asIterable().asSequence()
|
||||||
|
}).toList()
|
||||||
|
if (bypassList.isNotEmpty()) {
|
||||||
|
result.append("[bypass_list]\n")
|
||||||
|
result.append(bypassList.joinToString("\n"))
|
||||||
|
result.append('\n')
|
||||||
|
}
|
||||||
|
if (proxyList.isNotEmpty()) {
|
||||||
|
result.append("[proxy_list]\n")
|
||||||
|
result.append(proxyList.joinToString("\n"))
|
||||||
|
result.append('\n')
|
||||||
|
}
|
||||||
|
result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" })
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.acl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||||
|
companion object {
|
||||||
|
private const val KEY_ROUTE = "route"
|
||||||
|
|
||||||
|
fun schedule(route: String) = WorkManager.getInstance().enqueueUniqueWork(route, ExistingWorkPolicy.REPLACE,
|
||||||
|
OneTimeWorkRequestBuilder<AclSyncer>().run {
|
||||||
|
setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
|
||||||
|
setConstraints(Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||||
|
.setRequiresCharging(true)
|
||||||
|
.build())
|
||||||
|
setInitialDelay(10, TimeUnit.SECONDS)
|
||||||
|
build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext get() = Dispatchers.IO
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result = try {
|
||||||
|
val route = inputData.getString(KEY_ROUTE)!!
|
||||||
|
val acl = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openStream().bufferedReader()
|
||||||
|
.use { it.readText() }
|
||||||
|
Acl.getFile(route).printWriter().use { it.write(acl) }
|
||||||
|
Result.success()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Result.retry()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,44 +18,50 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.aidl
|
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.os.DeadObjectException
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import com.github.shadowsocks.LocalVpnService
|
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||||
import com.github.shadowsocks.bg.BaseService
|
import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
|
||||||
import com.github.shadowsocks.utils.Action
|
import org.amnezia.vpn.shadowsocks.core.bg.TransproxyService
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.bg.VpnService
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This object should be compact as it will not get GC-ed.
|
* This object should be compact as it will not get GC-ed.
|
||||||
*/
|
*/
|
||||||
class ShadowsocksConnection(
|
class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||||
private val handler: Handler = Handler(),
|
private var listenForDeath: Boolean = false) :
|
||||||
private var listenForDeath: Boolean = false
|
ServiceConnection, IBinder.DeathRecipient {
|
||||||
) : ServiceConnection,
|
|
||||||
IBinder.DeathRecipient {
|
|
||||||
companion object {
|
companion object {
|
||||||
val serviceClass = LocalVpnService::class.java
|
val serviceClass get() = when (DataStore.serviceMode) {
|
||||||
|
Key.modeProxy -> ProxyService::class
|
||||||
|
Key.modeVpn -> VpnService::class
|
||||||
|
Key.modeTransproxy -> TransproxyService::class
|
||||||
|
else -> throw UnknownError()
|
||||||
|
}.java
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
||||||
fun trafficUpdated(profileId: Long, stats: TrafficStats) {}
|
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
|
||||||
fun trafficPersisted(profileId: Long) {}
|
fun trafficPersisted(profileId: Long) { }
|
||||||
|
|
||||||
fun onServiceConnected(service: IShadowsocksService)
|
fun onServiceConnected(service: IShadowsocksService)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Different from Android framework, this method will be called even when you call `detachService`.
|
* Different from Android framework, this method will be called even when you call `detachService`.
|
||||||
*/
|
*/
|
||||||
fun onServiceDisconnected() {}
|
fun onServiceDisconnected() { }
|
||||||
|
fun onBinderDied() { }
|
||||||
fun onBinderDied() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var connectionActive = false
|
private var connectionActive = false
|
||||||
|
@ -64,16 +70,14 @@ class ShadowsocksConnection(
|
||||||
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
|
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
|
||||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||||
val callback = callback ?: return
|
val callback = callback ?: return
|
||||||
handler.post {
|
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
|
||||||
callback.stateChanged(BaseService.State.values()[state], profileName, msg)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||||
val callback = callback ?: return
|
val callback = callback ?: return
|
||||||
handler.post { callback.trafficUpdated(profileId, stats) }
|
handler.post {
|
||||||
|
callback.trafficUpdated(profileId, stats)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trafficPersisted(profileId: Long) {
|
override fun trafficPersisted(profileId: Long) {
|
||||||
val callback = callback ?: return
|
val callback = callback ?: return
|
||||||
handler.post { callback.trafficPersisted(profileId) }
|
handler.post { callback.trafficPersisted(profileId) }
|
||||||
|
@ -83,30 +87,25 @@ class ShadowsocksConnection(
|
||||||
|
|
||||||
var bandwidthTimeout = 0L
|
var bandwidthTimeout = 0L
|
||||||
set(value) {
|
set(value) {
|
||||||
try {
|
val service = service
|
||||||
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)
|
if (bandwidthTimeout != value && service != null)
|
||||||
else service?.stopListeningForBandwidth(serviceCallback)
|
if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
|
||||||
} catch (_: RemoteException) {
|
service.stopListeningForBandwidth(serviceCallback)
|
||||||
}
|
} catch (_: DeadObjectException) { }
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var service: IShadowsocksService? = null
|
var service: IShadowsocksService? = null
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||||
this.binder = binder
|
this.binder = binder
|
||||||
|
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||||
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||||
this.service = service
|
this.service = service
|
||||||
try {
|
if (!callbackRegistered) try {
|
||||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
|
||||||
check(!callbackRegistered)
|
|
||||||
service.registerCallback(serviceCallback)
|
service.registerCallback(serviceCallback)
|
||||||
callbackRegistered = true
|
callbackRegistered = true
|
||||||
if (bandwidthTimeout > 0) service.startListeningForBandwidth(
|
if (bandwidthTimeout > 0) service.startListeningForBandwidth(serviceCallback, bandwidthTimeout)
|
||||||
serviceCallback,
|
} catch (_: RemoteException) { }
|
||||||
bandwidthTimeout
|
|
||||||
)
|
|
||||||
} catch (_: RemoteException) {
|
|
||||||
}
|
|
||||||
callback!!.onServiceConnected(service)
|
callback!!.onServiceConnected(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +118,6 @@ class ShadowsocksConnection(
|
||||||
|
|
||||||
override fun binderDied() {
|
override fun binderDied() {
|
||||||
service = null
|
service = null
|
||||||
callbackRegistered = false
|
|
||||||
callback?.also { handler.post(it::onBinderDied) }
|
callback?.also { handler.post(it::onBinderDied) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,8 +125,7 @@ class ShadowsocksConnection(
|
||||||
val service = service
|
val service = service
|
||||||
if (service != null && callbackRegistered) try {
|
if (service != null && callbackRegistered) try {
|
||||||
service.unregisterCallback(serviceCallback)
|
service.unregisterCallback(serviceCallback)
|
||||||
} catch (_: RemoteException) {
|
} catch (_: RemoteException) { }
|
||||||
}
|
|
||||||
callbackRegistered = false
|
callbackRegistered = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,15 +142,11 @@ class ShadowsocksConnection(
|
||||||
unregisterCallback()
|
unregisterCallback()
|
||||||
if (connectionActive) try {
|
if (connectionActive) try {
|
||||||
context.unbindService(this)
|
context.unbindService(this)
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) { } // ignore
|
||||||
} // ignore
|
|
||||||
connectionActive = false
|
connectionActive = false
|
||||||
if (listenForDeath) binder?.unlinkToDeath(this, 0)
|
if (listenForDeath) binder?.unlinkToDeath(this, 0)
|
||||||
binder = null
|
binder = null
|
||||||
try {
|
|
||||||
service?.stopListeningForBandwidth(serviceCallback)
|
service?.stopListeningForBandwidth(serviceCallback)
|
||||||
} catch (_: RemoteException) {
|
|
||||||
}
|
|
||||||
service = null
|
service = null
|
||||||
callback = null
|
callback = null
|
||||||
}
|
}
|
|
@ -18,34 +18,31 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.aidl
|
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
data class TrafficStats(
|
data class TrafficStats(
|
||||||
// Bytes per second
|
// Bytes per second
|
||||||
var txRate: Long = 0L, var rxRate: Long = 0L,
|
var txRate: Long = 0L,
|
||||||
|
var rxRate: Long = 0L,
|
||||||
|
|
||||||
// Bytes for the current session
|
// Bytes for the current session
|
||||||
var txTotal: Long = 0L, var rxTotal: Long = 0L) : Parcelable {
|
var txTotal: Long = 0L,
|
||||||
operator fun plus(other: TrafficStats) = TrafficStats(txRate + other.txRate,
|
var rxTotal: Long = 0L
|
||||||
rxRate + other.rxRate,
|
) : Parcelable {
|
||||||
txTotal + other.txTotal,
|
operator fun plus(other: TrafficStats) = TrafficStats(
|
||||||
rxTotal + other.rxTotal)
|
txRate + other.txRate, rxRate + other.rxRate,
|
||||||
|
txTotal + other.txTotal, rxTotal + other.rxTotal)
|
||||||
constructor(parcel: Parcel) : this(parcel.readLong(),
|
|
||||||
parcel.readLong(),
|
|
||||||
parcel.readLong(),
|
|
||||||
parcel.readLong())
|
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readLong(), parcel.readLong(), parcel.readLong())
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeLong(txRate)
|
parcel.writeLong(txRate)
|
||||||
parcel.writeLong(rxRate)
|
parcel.writeLong(rxRate)
|
||||||
parcel.writeLong(txTotal)
|
parcel.writeLong(txTotal)
|
||||||
parcel.writeLong(rxTotal)
|
parcel.writeLong(rxTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents() = 0
|
override fun describeContents() = 0
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<TrafficStats> {
|
companion object CREATOR : Parcelable.Creator<TrafficStats> {
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -26,16 +26,21 @@ import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import com.github.shadowsocks.Core
|
|
||||||
import com.github.shadowsocks.Core.app
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||||
import com.github.shadowsocks.aidl.TrafficStats
|
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||||
import com.github.shadowsocks.net.HostsFile
|
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||||
import com.github.shadowsocks.preference.DataStore
|
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||||
import com.github.shadowsocks.utils.*
|
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.BindException
|
||||||
|
import java.net.InetAddress
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -50,26 +55,24 @@ object BaseService {
|
||||||
* Idle state is only used by UI and will never be returned by BaseService.
|
* Idle state is only used by UI and will never be returned by BaseService.
|
||||||
*/
|
*/
|
||||||
Idle,
|
Idle,
|
||||||
Connecting(true), Connected(true), Stopping, Stopped,
|
Connecting(true),
|
||||||
|
Connected(true),
|
||||||
|
Stopping,
|
||||||
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
const val CONFIG_FILE = "shadowsocks.conf"
|
const val CONFIG_FILE = "shadowsocks.conf"
|
||||||
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
|
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
|
||||||
|
|
||||||
interface ExpectedException
|
|
||||||
class ExpectedExceptionWrapper(e: Exception) : Exception(e.localizedMessage, e),
|
|
||||||
ExpectedException
|
|
||||||
|
|
||||||
class Data internal constructor(private val service: Interface) {
|
class Data internal constructor(private val service: Interface) {
|
||||||
var state = State.Stopped
|
var state = State.Stopped
|
||||||
var processes: GuardedProcessPool? = null
|
var processes: GuardedProcessPool? = null
|
||||||
var proxy: ProxyInstance? = null
|
var proxy: ProxyInstance? = null
|
||||||
// no udpFallback. xinlake
|
var udpFallback: ProxyInstance? = null
|
||||||
|
|
||||||
var notification: ServiceNotification? = null
|
var notification: ServiceNotification? = null
|
||||||
val closeReceiver = broadcastReceiver { _, intent ->
|
val closeReceiver = broadcastReceiver { _, intent ->
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_SHUTDOWN -> service.persistStats()
|
|
||||||
Action.RELOAD -> service.forceLoad()
|
Action.RELOAD -> service.forceLoad()
|
||||||
else -> service.stopRunner()
|
else -> service.stopRunner()
|
||||||
}
|
}
|
||||||
|
@ -86,16 +89,15 @@ object BaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), CoroutineScope, AutoCloseable {
|
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
|
||||||
private val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||||
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
|
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
|
||||||
super.onCallbackDied(callback, cookie)
|
super.onCallbackDied(callback, cookie)
|
||||||
stopListeningForBandwidth(callback ?: return)
|
stopListeningForBandwidth(callback ?: return)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||||
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
private val handler = Handler()
|
||||||
private var looper: Job? = null
|
|
||||||
|
|
||||||
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||||
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
|
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
|
||||||
|
@ -105,27 +107,24 @@ object BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
|
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
|
||||||
val count = callbacks.beginBroadcast()
|
repeat(callbacks.beginBroadcast()) {
|
||||||
try {
|
|
||||||
repeat(count) {
|
|
||||||
try {
|
try {
|
||||||
work(callbacks.getBroadcastItem(it))
|
work(callbacks.getBroadcastItem(it))
|
||||||
} catch (_: RemoteException) {
|
} catch (_: DeadObjectException) {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
printLog(e)
|
printLog(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
callbacks.finishBroadcast()
|
callbacks.finishBroadcast()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loop() {
|
private fun registerTimeout() {
|
||||||
while (true) {
|
handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: return)
|
||||||
// delay(bandwidthListeners.values.min() ?: return)
|
}
|
||||||
delay(5000)
|
private fun onTimeout() {
|
||||||
val proxies = listOfNotNull(data?.proxy)
|
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||||
val stats = proxies.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
val stats = proxies
|
||||||
|
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||||
.filter { it.second != null }
|
.filter { it.second != null }
|
||||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||||
|
@ -137,36 +136,38 @@ object BaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
registerTimeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
|
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
|
||||||
launch {
|
val wasEmpty = bandwidthListeners.isEmpty()
|
||||||
if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(cb.asBinder(), timeout) == null)) {
|
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
|
||||||
check(looper == null)
|
if (wasEmpty) registerTimeout()
|
||||||
looper = launch { loop() }
|
if (data?.state != State.Connected) return
|
||||||
}
|
|
||||||
if (data?.state != State.Connected) return@launch
|
|
||||||
var sum = TrafficStats()
|
var sum = TrafficStats()
|
||||||
val data = data
|
val data = data
|
||||||
val proxy = data?.proxy ?: return@launch
|
val proxy = data?.proxy ?: return
|
||||||
proxy.trafficMonitor?.out.also { stats ->
|
proxy.trafficMonitor?.out.also { stats ->
|
||||||
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
|
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
|
||||||
sum += stats
|
sum += stats
|
||||||
stats
|
stats
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
data.udpFallback?.also { udpFallback ->
|
||||||
|
udpFallback.trafficMonitor?.out.also { stats ->
|
||||||
|
cb.trafficUpdated(udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||||
|
sum += stats
|
||||||
|
stats
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
cb.trafficUpdated(0, sum)
|
cb.trafficUpdated(0, sum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||||
launch {
|
|
||||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||||
looper!!.cancel()
|
handler.removeCallbacksAndMessages(null)
|
||||||
looper = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +189,7 @@ object BaseService {
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
callbacks.kill()
|
callbacks.kill()
|
||||||
cancel()
|
handler.removeCallbacksAndMessages(null)
|
||||||
data = null
|
data = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,13 +199,13 @@ object BaseService {
|
||||||
val tag: String
|
val tag: String
|
||||||
fun createNotification(profileName: String): ServiceNotification
|
fun createNotification(profileName: String): ServiceNotification
|
||||||
|
|
||||||
fun onBind(intent: Intent): IBinder? =
|
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
|
||||||
if (intent.action == Action.SERVICE) data.binder else null
|
|
||||||
|
|
||||||
fun forceLoad() {
|
fun forceLoad() {
|
||||||
val (profile, fallback) = Core.currentProfile
|
val (profile, fallback) = Core.currentProfile
|
||||||
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
||||||
if (profile.host.isEmpty() || profile.password.isEmpty() || fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
|
if (profile.host.isEmpty() || profile.password.isEmpty() ||
|
||||||
|
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
|
||||||
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -212,21 +213,25 @@ object BaseService {
|
||||||
when {
|
when {
|
||||||
s == State.Stopped -> startRunner()
|
s == State.Stopped -> startRunner()
|
||||||
s.canStop -> stopRunner(true)
|
s.canStop -> stopRunner(true)
|
||||||
// else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
|
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
|
||||||
|
|
||||||
suspend fun startProcesses(hosts: HostsFile) {
|
suspend fun startProcesses() {
|
||||||
val configRoot =
|
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||||
(if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
|
||||||
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
|
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
|
||||||
|
val udpFallback = data.udpFallback
|
||||||
data.proxy!!.start(this,
|
data.proxy!!.start(this,
|
||||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||||
File(configRoot, CONFIG_FILE),
|
File(configRoot, CONFIG_FILE),
|
||||||
"-u")
|
if (udpFallback == null) "-u" else null)
|
||||||
|
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
|
||||||
|
udpFallback?.start(this,
|
||||||
|
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||||
|
File(configRoot, CONFIG_FILE_UDP),
|
||||||
|
"-U")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startRunner() {
|
fun startRunner() {
|
||||||
|
@ -247,7 +252,6 @@ object BaseService {
|
||||||
// channge the state
|
// channge the state
|
||||||
data.changeState(State.Stopping)
|
data.changeState(State.Stopping)
|
||||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
// Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
|
|
||||||
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
|
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
|
||||||
this@Interface as Service
|
this@Interface as Service
|
||||||
// we use a coroutineScope here to allow clean-up in parallel
|
// we use a coroutineScope here to allow clean-up in parallel
|
||||||
|
@ -263,11 +267,12 @@ object BaseService {
|
||||||
data.notification?.destroy()
|
data.notification?.destroy()
|
||||||
data.notification = null
|
data.notification = null
|
||||||
|
|
||||||
val ids = listOfNotNull(data.proxy).map {
|
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
|
||||||
it.shutdown(this)
|
it.shutdown(this)
|
||||||
it.profile.id
|
it.profile.id
|
||||||
}
|
}
|
||||||
data.proxy = null
|
data.proxy = null
|
||||||
|
data.udpFallback = null
|
||||||
data.binder.trafficPersisted(ids)
|
data.binder.trafficPersisted(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,17 +280,12 @@ object BaseService {
|
||||||
data.changeState(State.Stopped, msg)
|
data.changeState(State.Stopped, msg)
|
||||||
|
|
||||||
// stop the service if nothing has bound to it
|
// stop the service if nothing has bound to it
|
||||||
if (restart) startRunner() else {
|
if (restart) startRunner() else stopSelf()
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistStats() =
|
suspend fun preInit() { }
|
||||||
listOfNotNull(data.proxy).forEach { it.trafficMonitor?.persistStats(it.profile.id) }
|
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
|
||||||
|
|
||||||
suspend fun preInit() {}
|
|
||||||
suspend fun resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host)
|
|
||||||
suspend fun openConnection(url: URL) = url.openConnection()
|
suspend fun openConnection(url: URL) = url.openConnection()
|
||||||
|
|
||||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
@ -299,10 +299,11 @@ object BaseService {
|
||||||
stopRunner(false, getString(R.string.profile_empty))
|
stopRunner(false, getString(R.string.profile_empty))
|
||||||
return Service.START_NOT_STICKY
|
return Service.START_NOT_STICKY
|
||||||
}
|
}
|
||||||
val (profile, _) = profilePair
|
val (profile, fallback) = profilePair
|
||||||
profile.name = profile.formattedName // save name for later queries
|
profile.name = profile.formattedName // save name for later queries
|
||||||
val proxy = ProxyInstance(profile)
|
val proxy = ProxyInstance(profile)
|
||||||
data.proxy = proxy
|
data.proxy = proxy
|
||||||
|
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||||
|
|
||||||
if (!data.closeReceiverRegistered) {
|
if (!data.closeReceiverRegistered) {
|
||||||
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||||
|
@ -314,29 +315,35 @@ object BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
data.notification = createNotification(profile.formattedName)
|
data.notification = createNotification(profile.formattedName)
|
||||||
// Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
|
|
||||||
|
|
||||||
data.changeState(State.Connecting)
|
data.changeState(State.Connecting)
|
||||||
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
Executable.killAll() // clean up old processes
|
Executable.killAll() // clean up old processes
|
||||||
preInit()
|
preInit()
|
||||||
val hosts = HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")
|
proxy.init(this@Interface)
|
||||||
proxy.init(this@Interface, hosts)
|
data.udpFallback?.init(this@Interface)
|
||||||
|
|
||||||
data.processes = GuardedProcessPool {
|
data.processes = GuardedProcessPool {
|
||||||
printLog(it)
|
printLog(it)
|
||||||
stopRunner(false, it.readableMessage)
|
stopRunner(false, it.readableMessage)
|
||||||
}
|
}
|
||||||
startProcesses(hosts)
|
startProcesses()
|
||||||
// proxy.scheduleUpdate() // XinLake. Bypass-LAN only
|
|
||||||
|
proxy.scheduleUpdate()
|
||||||
|
data.udpFallback?.scheduleUpdate()
|
||||||
|
|
||||||
data.changeState(State.Connected)
|
data.changeState(State.Connected)
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
// if the job was cancelled, it is canceller's responsibility to call stopRunner
|
// if the job was cancelled, it is canceller's responsibility to call stopRunner
|
||||||
} catch (_: UnknownHostException) {
|
} catch (_: UnknownHostException) {
|
||||||
stopRunner(false, getString(R.string.invalid_server))
|
stopRunner(false, getString(R.string.invalid_server))
|
||||||
} catch (exc: Throwable) {
|
} catch (exc: Throwable) {
|
||||||
if (exc is ExpectedException) exc.printStackTrace() else printLog(exc)
|
if (exc !is PluginManager.PluginNotFoundException &&
|
||||||
|
exc !is BindException &&
|
||||||
|
exc !is VpnService.NullConnectionException) {
|
||||||
|
printLog(exc)
|
||||||
|
}
|
||||||
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
|
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
|
||||||
} finally {
|
} finally {
|
||||||
data.connectingJob = null
|
data.connectingJob = null
|
|
@ -18,25 +18,25 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
object Executable {
|
object Executable {
|
||||||
// libredsocks.so is not required. xinlake
|
const val REDSOCKS = "libredsocks.so"
|
||||||
const val SS_LOCAL = "libss-local.so"
|
const val SS_LOCAL = "libss-local.so"
|
||||||
const val TUN2SOCKS = "libtun2socks.so"
|
const val TUN2SOCKS = "libtun2socks.so"
|
||||||
|
|
||||||
private val EXECUTABLES = setOf(SS_LOCAL, TUN2SOCKS)
|
private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
|
||||||
|
|
||||||
fun killAll() {
|
fun killAll() {
|
||||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
|
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||||
?: return) {
|
|
||||||
val exe = File(try {
|
val exe = File(try {
|
||||||
File(process, "cmdline").inputStream().bufferedReader().readText()
|
File(process, "cmdline").inputStream().bufferedReader().readText()
|
||||||
} catch (_: IOException) {
|
} catch (_: IOException) {
|
||||||
|
@ -47,8 +47,6 @@ object Executable {
|
||||||
} catch (e: ErrnoException) {
|
} catch (e: ErrnoException) {
|
||||||
if (e.errno != OsConstants.ESRCH) {
|
if (e.errno != OsConstants.ESRCH) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
// Crashlytics.log(Log.WARN, "kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed")
|
|
||||||
// Crashlytics.logException(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,15 +18,17 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import com.github.shadowsocks.Core
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -60,33 +62,22 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||||
val exitChannel = Channel<Int>()
|
val exitChannel = Channel<Int>()
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
thread(name = "stderr-$cmdName") {
|
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
|
||||||
streamLogger(process.errorStream) {
|
|
||||||
// Crashlytics.log(Log.ERROR, cmdName, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread(name = "stdout-$cmdName") {
|
thread(name = "stdout-$cmdName") {
|
||||||
streamLogger(process.inputStream) {
|
streamLogger(process.inputStream) { Log.i(cmdName, it) }
|
||||||
// Crashlytics.log(Log.VERBOSE, cmdName, it)
|
|
||||||
}
|
|
||||||
// this thread also acts as a daemon thread for waitFor
|
// this thread also acts as a daemon thread for waitFor
|
||||||
runBlocking { exitChannel.send(process.waitFor()) }
|
runBlocking { exitChannel.send(process.waitFor()) }
|
||||||
}
|
}
|
||||||
val startTime = SystemClock.elapsedRealtime()
|
val startTime = SystemClock.elapsedRealtime()
|
||||||
val exitCode = exitChannel.receive()
|
val exitCode = exitChannel.receive()
|
||||||
running = false
|
running = false
|
||||||
when {
|
if (SystemClock.elapsedRealtime() - startTime < 1000) {
|
||||||
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException("$cmdName exits too fast (exit code: $exitCode)")
|
throw IOException("$cmdName exits too fast (exit code: $exitCode)")
|
||||||
// exitCode == 128 + OsConstants.SIGKILL -> Crashlytics.log(Log.WARN, TAG, "$cmdName was killed")
|
|
||||||
// else -> Crashlytics.logException(IOException("$cmdName unexpectedly exits with code $exitCode"))
|
|
||||||
}
|
}
|
||||||
// Crashlytics.log(Log.DEBUG, TAG, "restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
|
|
||||||
start()
|
start()
|
||||||
running = true
|
|
||||||
onRestartCallback?.invoke()
|
onRestartCallback?.invoke()
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
|
|
||||||
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
|
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
|
||||||
} finally {
|
} finally {
|
||||||
if (running) withContext(NonCancellable) {
|
if (running) withContext(NonCancellable) {
|
||||||
|
@ -114,7 +105,6 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
|
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
|
||||||
// Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
|
|
||||||
Guard(cmd).apply {
|
Guard(cmd).apply {
|
||||||
start() // if start fails, IOException will be thrown directly
|
start() // if start fails, IOException will be thrown directly
|
||||||
launch { looper(onRestartCallback) }
|
launch { looper(onRestartCallback) }
|
|
@ -18,39 +18,47 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import com.github.shadowsocks.net.HostsFile
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
import com.github.shadowsocks.net.LocalDnsServer
|
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||||
import com.github.shadowsocks.net.Socks5Endpoint
|
import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
|
||||||
import com.github.shadowsocks.preference.DataStore
|
import org.amnezia.vpn.shadowsocks.core.net.Socks5Endpoint
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
|
||||||
object LocalDnsService {
|
object LocalDnsService {
|
||||||
private val googleApisTester =
|
private val googleApisTester =
|
||||||
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
|
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
|
||||||
|
private val chinaIpList by lazy {
|
||||||
|
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
|
||||||
|
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
|
||||||
|
}
|
||||||
|
|
||||||
private val servers = WeakHashMap<Interface, LocalDnsServer>()
|
private val servers = WeakHashMap<Interface, LocalDnsServer>()
|
||||||
|
|
||||||
interface Interface : BaseService.Interface {
|
interface Interface : BaseService.Interface {
|
||||||
override suspend fun startProcesses(hosts: HostsFile) {
|
override suspend fun startProcesses() {
|
||||||
super.startProcesses(hosts)
|
super.startProcesses()
|
||||||
val profile = data.proxy!!.profile
|
val profile = data.proxy!!.profile
|
||||||
val dns = try {
|
val dns = URI("dns://${profile.remoteDns}")
|
||||||
URI("dns://${profile.remoteDns}")
|
|
||||||
} catch (e: URISyntaxException) {
|
|
||||||
throw BaseService.ExpectedExceptionWrapper(e)
|
|
||||||
}
|
|
||||||
LocalDnsServer(this::resolver,
|
LocalDnsServer(this::resolver,
|
||||||
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||||
DataStore.proxyAddress,
|
DataStore.proxyAddress).apply {
|
||||||
hosts).apply {
|
|
||||||
tcp = !profile.udpdns
|
tcp = !profile.udpdns
|
||||||
forwardOnly = true
|
when (profile.route) {
|
||||||
|
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
|
||||||
|
remoteDomainMatcher = googleApisTester
|
||||||
|
localIpMatcher = chinaIpList
|
||||||
|
}
|
||||||
|
Acl.CHINALIST -> { }
|
||||||
|
else -> forwardOnly = true
|
||||||
|
}
|
||||||
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
|
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.acl.AclSyncer
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class sets up environment for ss-local.
|
||||||
|
*/
|
||||||
|
class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
|
||||||
|
private var configFile: File? = null
|
||||||
|
var trafficMonitor: TrafficMonitor? = null
|
||||||
|
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
|
||||||
|
val pluginPath by lazy { PluginManager.init(plugin) }
|
||||||
|
|
||||||
|
suspend fun init(service: BaseService.Interface) {
|
||||||
|
if (route == Acl.CUSTOM_RULES) withContext(Dispatchers.IO) {
|
||||||
|
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, service::openConnection))
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's hard to resolve DNS on a specific interface so we'll do it here
|
||||||
|
if (profile.host.parseNumericAddress() == null) {
|
||||||
|
while (true) try {
|
||||||
|
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
|
||||||
|
profile.host = io.await().firstOrNull()?.hostAddress ?: throw UnknownHostException()
|
||||||
|
return
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
|
||||||
|
if (!DataStore.hasArc0) throw e
|
||||||
|
Thread.yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
|
||||||
|
* device storage, depending on which is currently available.
|
||||||
|
*/
|
||||||
|
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
|
||||||
|
trafficMonitor = TrafficMonitor(stat)
|
||||||
|
|
||||||
|
this.configFile = configFile
|
||||||
|
val config = profile.toJson()
|
||||||
|
if (pluginPath != null) config.put("plugin", pluginPath).put("plugin_opts", plugin.toString())
|
||||||
|
configFile.writeText(config.toString())
|
||||||
|
|
||||||
|
val cmd = service.buildAdditionalArguments(arrayListOf(
|
||||||
|
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||||
|
"-b", DataStore.listenAddress,
|
||||||
|
"-l", DataStore.portProxy.toString(),
|
||||||
|
"-t", "600",
|
||||||
|
"-S", stat.absolutePath,
|
||||||
|
"-c", configFile.absolutePath))
|
||||||
|
if (extraFlag != null) cmd.add(extraFlag)
|
||||||
|
|
||||||
|
if (route != Acl.ALL) {
|
||||||
|
cmd += "--acl"
|
||||||
|
cmd += Acl.getFile(route).absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
|
||||||
|
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_LAN) cmd += "-D"
|
||||||
|
|
||||||
|
if (DataStore.tcpFastOpen) cmd += "--fast-open"
|
||||||
|
|
||||||
|
service.data.processes!!.start(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scheduleUpdate() {
|
||||||
|
if (route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncer.schedule(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shutdown(scope: CoroutineScope) {
|
||||||
|
trafficMonitor?.apply {
|
||||||
|
thread.shutdown(scope)
|
||||||
|
// Make sure update total traffic when stopping the runner
|
||||||
|
try {
|
||||||
|
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
||||||
|
val profile = ProfileManager.getProfile(profile.id) ?: return
|
||||||
|
profile.tx += current.txTotal
|
||||||
|
profile.rx += current.rxTotal
|
||||||
|
ProfileManager.updateProfile(profile)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
|
||||||
|
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == profile.id }
|
||||||
|
profile.tx += current.txTotal
|
||||||
|
profile.rx += current.rxTotal
|
||||||
|
profile.dirty = true
|
||||||
|
DirectBoot.update(profile)
|
||||||
|
DirectBoot.listenForUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trafficMonitor = null
|
||||||
|
configFile?.delete() // remove old config possibly in device storage
|
||||||
|
configFile = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* *
|
* *
|
||||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
* *
|
* *
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
* it under the terms of the GNU General Public License as published by *
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
@ -18,29 +18,25 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.preference
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import android.graphics.Typeface
|
import android.app.Service
|
||||||
import android.text.InputFilter
|
import android.content.Intent
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.EditText
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
|
|
||||||
object EditTextPreferenceModifiers {
|
/**
|
||||||
object Monospace : EditTextPreference.OnBindEditTextListener {
|
* Shadowsocks service at its minimum.
|
||||||
override fun onBindEditText(editText: EditText) {
|
*/
|
||||||
editText.typeface = Typeface.MONOSPACE
|
class ProxyService : Service(), BaseService.Interface {
|
||||||
}
|
override val data = BaseService.Data(this)
|
||||||
}
|
override val tag: String get() = "ShadowsocksProxyService"
|
||||||
|
override fun createNotification(profileName: String): ServiceNotification =
|
||||||
|
ServiceNotification(this, profileName, "service-proxy", true)
|
||||||
|
|
||||||
object Port : EditTextPreference.OnBindEditTextListener {
|
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||||
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||||
|
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||||
override fun onBindEditText(editText: EditText) {
|
override fun onDestroy() {
|
||||||
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
|
super.onDestroy()
|
||||||
editText.filters = portLengthFilter
|
data.binder.close()
|
||||||
editText.setSingleLine()
|
|
||||||
editText.setSelection(editText.text.length)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android < 8 VPN: always invisible because of VPN notification/icon
|
||||||
|
* Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||||
|
* Android 8+: always visible due to system limitations
|
||||||
|
* (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||||
|
*/
|
||||||
|
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||||
|
channel: String, private val visible: Boolean = false) {
|
||||||
|
private val keyGuard = (service as Context).getSystemService<KeyguardManager>()!!
|
||||||
|
private val nm by lazy { (service as Context).getSystemService<NotificationManager>()!! }
|
||||||
|
private val callback: IShadowsocksServiceCallback by lazy {
|
||||||
|
object : IShadowsocksServiceCallback.Stub() {
|
||||||
|
override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore
|
||||||
|
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||||
|
if (profileId != 0L) return
|
||||||
|
service as Context
|
||||||
|
val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate))
|
||||||
|
val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))
|
||||||
|
builder.setContentText("$txr↑\t$rxr↓")
|
||||||
|
style.bigText(service.getString(R.string.stat_summary, txr, rxr,
|
||||||
|
Formatter.formatFileSize(service, stats.txTotal),
|
||||||
|
Formatter.formatFileSize(service, stats.rxTotal)))
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
override fun trafficPersisted(profileId: Long) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||||
|
private var callbackRegistered = false
|
||||||
|
|
||||||
|
private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||||
|
.setWhen(0)
|
||||||
|
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||||
|
.setTicker(service.getString(R.string.forward_success))
|
||||||
|
.setContentTitle(profileName)
|
||||||
|
.setContentIntent(Core.configureIntent(service))
|
||||||
|
.setSmallIcon(R.drawable.ic_service_active)
|
||||||
|
private val style = NotificationCompat.BigTextStyle(builder).bigText("")
|
||||||
|
private var isVisible = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
service as Context
|
||||||
|
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||||
|
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||||
|
update(if (service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||||
|
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||||
|
service.registerReceiver(lockReceiver, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
if (visible && Build.VERSION.SDK_INT < 26) addAction(Intent.ACTION_USER_PRESENT)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update(action: String?, forceShow: Boolean = false) {
|
||||||
|
if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
|
||||||
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
|
setVisible(false, forceShow)
|
||||||
|
unregisterCallback() // unregister callback to save battery
|
||||||
|
}
|
||||||
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
|
setVisible(visible && !keyGuard.isKeyguardLocked, forceShow)
|
||||||
|
service.data.binder.registerCallback(callback)
|
||||||
|
service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||||
|
callbackRegistered = true
|
||||||
|
}
|
||||||
|
Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterCallback() {
|
||||||
|
if (callbackRegistered) {
|
||||||
|
service.data.binder.unregisterCallback(callback)
|
||||||
|
callbackRegistered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||||
|
if (isVisible != visible) {
|
||||||
|
isVisible = visible
|
||||||
|
builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||||
|
show()
|
||||||
|
} else if (forceShow) show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun show() = (service as Service).startForeground(1, builder.build())
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
(service as Service).unregisterReceiver(lockReceiver)
|
||||||
|
unregisterCallback()
|
||||||
|
service.stopForeground(true)
|
||||||
|
nm.cancel(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,15 +18,12 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import android.net.LocalSocket
|
import android.net.LocalSocket
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import com.github.shadowsocks.aidl.TrafficStats
|
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||||
import com.github.shadowsocks.database.ProfileManager
|
import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
|
||||||
import com.github.shadowsocks.net.LocalSocketListener
|
|
||||||
import com.github.shadowsocks.preference.DataStore
|
|
||||||
import com.github.shadowsocks.utils.DirectBoot
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
@ -55,7 +52,6 @@ class TrafficMonitor(statFile: File) {
|
||||||
var out = TrafficStats()
|
var out = TrafficStats()
|
||||||
private var timestampLast = 0L
|
private var timestampLast = 0L
|
||||||
private var dirty = false
|
private var dirty = false
|
||||||
private var persisted: TrafficStats? = null
|
|
||||||
|
|
||||||
fun requestUpdate(): Pair<TrafficStats, Boolean> {
|
fun requestUpdate(): Pair<TrafficStats, Boolean> {
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
@ -83,26 +79,4 @@ class TrafficMonitor(statFile: File) {
|
||||||
}
|
}
|
||||||
return Pair(out, updated)
|
return Pair(out, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistStats(id: Long) {
|
|
||||||
val current = current
|
|
||||||
check(persisted == null || persisted == current) { "Data loss occurred" }
|
|
||||||
persisted = current
|
|
||||||
|
|
||||||
try {
|
|
||||||
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
|
||||||
val profile = ProfileManager.getProfile(id) ?: return
|
|
||||||
profile.tx += current.txTotal
|
|
||||||
profile.rx += current.rxTotal
|
|
||||||
ProfileManager.updateProfile(profile)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
|
|
||||||
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == id }
|
|
||||||
profile.tx += current.txTotal
|
|
||||||
profile.rx += current.rxTotal
|
|
||||||
profile.dirty = true
|
|
||||||
DirectBoot.update(profile)
|
|
||||||
DirectBoot.listenForUnlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class TransproxyService : Service(), LocalDnsService.Interface {
|
||||||
|
override val data = BaseService.Data(this)
|
||||||
|
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||||
|
override fun createNotification(profileName: String): ServiceNotification =
|
||||||
|
ServiceNotification(this, profileName, "service-transproxy", true)
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||||
|
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
|
private fun startRedsocksDaemon() {
|
||||||
|
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
|
||||||
|
log_debug = off;
|
||||||
|
log_info = off;
|
||||||
|
log = stderr;
|
||||||
|
daemon = off;
|
||||||
|
redirector = iptables;
|
||||||
|
}
|
||||||
|
redsocks {
|
||||||
|
local_ip = ${DataStore.listenAddress};
|
||||||
|
local_port = ${DataStore.portTransproxy};
|
||||||
|
ip = 127.0.0.1;
|
||||||
|
port = ${DataStore.portProxy};
|
||||||
|
type = socks5;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
data.processes!!.start(listOf(
|
||||||
|
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun startProcesses() {
|
||||||
|
startRedsocksDaemon()
|
||||||
|
super.startProcesses()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
data.binder.close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,10 +18,11 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.bg
|
package org.amnezia.vpn.shadowsocks.core.bg
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.LocalSocket
|
import android.net.LocalSocket
|
||||||
import android.net.LocalSocketAddress
|
import android.net.LocalSocketAddress
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
|
@ -29,14 +30,16 @@ import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import com.github.shadowsocks.Core
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import org.amnezia.vpn.R
|
import org.amnezia.vpn.R
|
||||||
import com.github.shadowsocks.net.ConcurrentLocalSocketListener
|
import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
|
||||||
import com.github.shadowsocks.net.DefaultNetworkListener
|
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||||
import com.github.shadowsocks.net.HostsFile
|
import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
|
||||||
import com.github.shadowsocks.net.Subnet
|
import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
|
||||||
import com.github.shadowsocks.preference.DataStore
|
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||||
import com.github.shadowsocks.utils.printLog
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -45,7 +48,6 @@ import java.io.File
|
||||||
import java.io.FileDescriptor
|
import java.io.FileDescriptor
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import android.net.VpnService as BaseVpnService
|
import android.net.VpnService as BaseVpnService
|
||||||
|
|
||||||
|
@ -87,7 +89,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class NullConnectionException : NullPointerException(), BaseService.ExpectedException {
|
inner class NullConnectionException : NullPointerException() {
|
||||||
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,15 +101,16 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
private var conn: ParcelFileDescriptor? = null
|
private var conn: ParcelFileDescriptor? = null
|
||||||
private var worker: ProtectWorker? = null
|
private var worker: ProtectWorker? = null
|
||||||
private var active = false
|
private var active = false
|
||||||
// metered = false. xinlake
|
private var metered = false
|
||||||
private var underlyingNetwork: Network? = null
|
private var underlyingNetwork: Network? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
if (active) setUnderlyingNetworks(underlyingNetworks)
|
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
|
||||||
}
|
}
|
||||||
private val underlyingNetworks
|
private val underlyingNetworks
|
||||||
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered
|
get() =
|
||||||
underlyingNetwork?.let { arrayOf(it) }
|
// clearing underlyingNetworks makes Android 9+ consider the network to be metered
|
||||||
|
if (Build.VERSION.SDK_INT >= 28 && metered) null else underlyingNetwork?.let { arrayOf(it) }
|
||||||
|
|
||||||
override fun onBind(intent: Intent) = when (intent.action) {
|
override fun onBind(intent: Intent) = when (intent.action) {
|
||||||
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
||||||
|
@ -127,25 +130,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (DataStore.serviceMode == Key.modeVpn) {
|
||||||
if (prepare(this) != null) {
|
if (prepare(this) != null) {
|
||||||
// startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
} else {
|
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||||
return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopRunner()
|
stopRunner()
|
||||||
return Service.START_NOT_STICKY
|
return Service.START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
|
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
|
||||||
override suspend fun resolver(host: String) =
|
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
|
||||||
DnsResolverCompat.resolve(DefaultNetworkListener.get(), host)
|
|
||||||
|
|
||||||
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
|
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
|
||||||
|
|
||||||
override suspend fun startProcesses(hosts: HostsFile) {
|
override suspend fun startProcesses() {
|
||||||
worker = ProtectWorker().apply { start() }
|
worker = ProtectWorker().apply { start() }
|
||||||
super.startProcesses(hosts)
|
super.startProcesses()
|
||||||
sendFd(startVpn())
|
sendFd(startVpn())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,18 +167,39 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||||
builder.addRoute("::", 0)
|
builder.addRoute("::", 0)
|
||||||
}
|
}
|
||||||
|
val me = packageName
|
||||||
|
if (profile.proxyApps) {
|
||||||
|
profile.individual.split('\n')
|
||||||
|
.filter { it != me }
|
||||||
|
.forEach {
|
||||||
|
try {
|
||||||
|
if (profile.bypass) builder.addDisallowedApplication(it)
|
||||||
|
else builder.addAllowedApplication(it)
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
printLog(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (profile.bypass) {
|
||||||
|
builder.addDisallowedApplication(me)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.addDisallowedApplication(me)
|
||||||
|
}
|
||||||
|
|
||||||
// XinLake. bypass lan
|
when (profile.route) {
|
||||||
|
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
|
||||||
|
else -> {
|
||||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||||
val subnet = Subnet.fromString(it)!!
|
val subnet = Subnet.fromString(it)!!
|
||||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||||
}
|
}
|
||||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
||||||
|
|
||||||
active = true // possible race condition here?
|
|
||||||
if (Build.VERSION.SDK_INT >= 22) {
|
|
||||||
builder.setUnderlyingNetworks(underlyingNetworks)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metered = profile.metered
|
||||||
|
active = true // possible race condition here?
|
||||||
|
if (Build.VERSION.SDK_INT >= 22) builder.setUnderlyingNetworks(underlyingNetworks)
|
||||||
|
|
||||||
val conn = builder.establish() ?: throw NullConnectionException()
|
val conn = builder.establish() ?: throw NullConnectionException()
|
||||||
this.conn = conn
|
this.conn = conn
|
||||||
|
@ -199,6 +220,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
try {
|
try {
|
||||||
sendFd(conn.fileDescriptor)
|
sendFd(conn.fileDescriptor)
|
||||||
} catch (e: ErrnoException) {
|
} catch (e: ErrnoException) {
|
||||||
|
e.printStackTrace()
|
||||||
stopRunner(false, e.message)
|
stopRunner(false, e.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.database
|
package org.amnezia.vpn.shadowsocks.core.database
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
@ -62,8 +62,7 @@ class KeyValuePair() {
|
||||||
@Deprecated("Use long.", ReplaceWith("long"))
|
@Deprecated("Use long.", ReplaceWith("long"))
|
||||||
val int: Int?
|
val int: Int?
|
||||||
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
|
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
|
||||||
val long: Long?
|
val long: Long? get() = when (valueType) {
|
||||||
get() = when (valueType) {
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
|
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
|
||||||
TYPE_LONG -> ByteBuffer.wrap(value).long
|
TYPE_LONG -> ByteBuffer.wrap(value).long
|
||||||
|
@ -94,13 +93,11 @@ class KeyValuePair() {
|
||||||
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
|
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(value: Float): KeyValuePair {
|
fun put(value: Float): KeyValuePair {
|
||||||
valueType = TYPE_FLOAT
|
valueType = TYPE_FLOAT
|
||||||
this.value = ByteBuffer.allocate(4).putFloat(value).array()
|
this.value = ByteBuffer.allocate(4).putFloat(value).array()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Use long.")
|
@Deprecated("Use long.")
|
||||||
fun put(value: Int): KeyValuePair {
|
fun put(value: Int): KeyValuePair {
|
||||||
|
@ -108,26 +105,21 @@ class KeyValuePair() {
|
||||||
this.value = ByteBuffer.allocate(4).putInt(value).array()
|
this.value = ByteBuffer.allocate(4).putInt(value).array()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(value: Long): KeyValuePair {
|
fun put(value: Long): KeyValuePair {
|
||||||
valueType = TYPE_LONG
|
valueType = TYPE_LONG
|
||||||
this.value = ByteBuffer.allocate(8).putLong(value).array()
|
this.value = ByteBuffer.allocate(8).putLong(value).array()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(value: String): KeyValuePair {
|
fun put(value: String): KeyValuePair {
|
||||||
valueType = TYPE_STRING
|
valueType = TYPE_STRING
|
||||||
this.value = value.toByteArray()
|
this.value = value.toByteArray()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(value: Set<String>): KeyValuePair {
|
fun put(value: Set<String>): KeyValuePair {
|
||||||
valueType = TYPE_STRING_SET
|
valueType = TYPE_STRING_SET
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
val intBuffer = ByteBuffer.allocate(4)
|
|
||||||
for (v in value) {
|
for (v in value) {
|
||||||
intBuffer.rewind()
|
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
|
||||||
stream.write(intBuffer.putInt(v.length).array())
|
|
||||||
stream.write(v.toByteArray())
|
stream.write(v.toByteArray())
|
||||||
}
|
}
|
||||||
this.value = stream.toByteArray()
|
this.value = stream.toByteArray()
|
|
@ -18,46 +18,52 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.database
|
package org.amnezia.vpn.shadowsocks.core.database
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.github.shadowsocks.Core.app
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
|
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
|
||||||
import com.github.shadowsocks.utils.Key
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Database(entities = [Profile::class, KeyValuePair::class], version = 1000)
|
@Database(entities = [Profile::class, KeyValuePair::class], version = 29)
|
||||||
abstract class PrivateDatabase : RoomDatabase() {
|
abstract class PrivateDatabase : RoomDatabase() {
|
||||||
companion object {
|
companion object {
|
||||||
private val instance by lazy {
|
private val instance by lazy {
|
||||||
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE).apply {
|
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
|
||||||
addMigrations(Migration1000)
|
.addMigrations(
|
||||||
allowMainThreadQueries()
|
Migration26,
|
||||||
enableMultiInstanceInvalidation()
|
Migration27,
|
||||||
fallbackToDestructiveMigration()
|
Migration28
|
||||||
setQueryExecutor { GlobalScope.launch { it.run() } }
|
)
|
||||||
}.build()
|
.fallbackToDestructiveMigration()
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val profileDao get() = instance.profileDao()
|
val profileDao get() = instance.profileDao()
|
||||||
val kvPairDao get() = instance.keyValuePairDao()
|
val kvPairDao get() = instance.keyValuePairDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun profileDao(): Profile.Dao
|
abstract fun profileDao(): Profile.Dao
|
||||||
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||||
|
|
||||||
object Migration1000 : RecreateSchemaMigration(999,
|
object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
|
||||||
1000,
|
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)",
|
||||||
"Profile",
|
"`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") {
|
||||||
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
|
|
||||||
"`id`, `name`, `host`, `remotePort`, `password`, `method`, `remoteDns`, `udpdns`, `ipv6`, `tx`, `rx`, `userOrder`") {
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
super.migrate(database)
|
super.migrate(database)
|
||||||
PublicDatabase.Migration3.migrate(database)
|
PublicDatabase.Migration3.migrate(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
object Migration27 : Migration(26, 27) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) =
|
||||||
|
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `udpFallback` INTEGER")
|
||||||
|
}
|
||||||
|
object Migration28 : Migration(27, 28) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) =
|
||||||
|
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `metered` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.database
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.LongSparseArray
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.room.*
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
|
||||||
|
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.json.JSONTokener
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Parcelize
|
||||||
|
data class Profile(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var id: Long = 0,
|
||||||
|
var name: String? = "",
|
||||||
|
var host: String = "155.94.174.51",
|
||||||
|
var remotePort: Int = 444,
|
||||||
|
var password: String = "789456123",
|
||||||
|
var method: String = "aes-256-cfb",
|
||||||
|
var route: String = "all",
|
||||||
|
var remoteDns: String = "dns.google",
|
||||||
|
var proxyApps: Boolean = false,
|
||||||
|
var bypass: Boolean = false,
|
||||||
|
var udpdns: Boolean = false,
|
||||||
|
var ipv6: Boolean = true,
|
||||||
|
@TargetApi(28)
|
||||||
|
var metered: Boolean = false,
|
||||||
|
var individual: String = "",
|
||||||
|
var tx: Long = 0,
|
||||||
|
var rx: Long = 0,
|
||||||
|
var userOrder: Long = 0,
|
||||||
|
var plugin: String? = null,
|
||||||
|
var udpFallback: Long? = null,
|
||||||
|
|
||||||
|
@Ignore // not persisted in db, only used by direct boot
|
||||||
|
var dirty: Boolean = false
|
||||||
|
) : Parcelable, Serializable {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ShadowParser"
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
private val pattern =
|
||||||
|
"""(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
|
||||||
|
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
|
||||||
|
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
|
||||||
|
|
||||||
|
fun findAllUrls(data: CharSequence?, feature: Profile? = null) = pattern.findAll(data ?: "").map {
|
||||||
|
val uri = it.value.toUri()
|
||||||
|
try {
|
||||||
|
if (uri.userInfo == null) {
|
||||||
|
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
|
||||||
|
if (match != null) {
|
||||||
|
val profile = Profile()
|
||||||
|
feature?.copyFeatureSettingsTo(profile)
|
||||||
|
profile.method = match.groupValues[1].toLowerCase()
|
||||||
|
profile.password = match.groupValues[2]
|
||||||
|
profile.host = match.groupValues[3]
|
||||||
|
profile.remotePort = match.groupValues[4].toInt()
|
||||||
|
profile.plugin = uri.getQueryParameter(Key.plugin)
|
||||||
|
profile.name = uri.fragment
|
||||||
|
profile
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unrecognized URI: ${it.value}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
|
||||||
|
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
|
||||||
|
if (match != null) {
|
||||||
|
val profile = Profile()
|
||||||
|
feature?.copyFeatureSettingsTo(profile)
|
||||||
|
profile.method = match.groupValues[1]
|
||||||
|
profile.password = match.groupValues[2]
|
||||||
|
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
|
||||||
|
try {
|
||||||
|
val javaURI = URI(it.value)
|
||||||
|
profile.host = javaURI.host ?: ""
|
||||||
|
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
|
||||||
|
profile.host = profile.host.substring(1, profile.host.length - 1)
|
||||||
|
profile.remotePort = javaURI.port
|
||||||
|
profile.plugin = uri.getQueryParameter(Key.plugin)
|
||||||
|
profile.name = uri.fragment ?: ""
|
||||||
|
profile
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
Log.e(TAG, "Invalid URI: ${it.value}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unknown user info: ${it.value}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Log.e(TAG, "Invalid base64 detected: ${it.value}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.filterNotNull()
|
||||||
|
|
||||||
|
private class JsonParser(private val feature: Profile? = null) : ArrayList<Profile>() {
|
||||||
|
private val fallbackMap = mutableMapOf<Profile, Profile>()
|
||||||
|
|
||||||
|
private fun tryParse(json: JSONObject, fallback: Boolean = false): Profile? {
|
||||||
|
val host = json.optString("server")
|
||||||
|
if (host.isNullOrEmpty()) return null
|
||||||
|
val remotePort = json.optInt("server_port")
|
||||||
|
if (remotePort <= 0) return null
|
||||||
|
val password = json.optString("password")
|
||||||
|
if (password.isNullOrEmpty()) return null
|
||||||
|
val method = json.optString("method")
|
||||||
|
if (method.isNullOrEmpty()) return null
|
||||||
|
return Profile().also {
|
||||||
|
it.host = host
|
||||||
|
it.remotePort = remotePort
|
||||||
|
it.password = password
|
||||||
|
it.method = method
|
||||||
|
}.apply {
|
||||||
|
feature?.copyFeatureSettingsTo(this)
|
||||||
|
val id = json.optString("plugin")
|
||||||
|
if (!id.isNullOrEmpty()) {
|
||||||
|
plugin = PluginOptions(id, json.optString("plugin_opts")).toString(false)
|
||||||
|
}
|
||||||
|
name = json.optString("remarks")
|
||||||
|
route = json.optString("route", route)
|
||||||
|
if (fallback) return@apply
|
||||||
|
remoteDns = json.optString("remote_dns", remoteDns)
|
||||||
|
ipv6 = json.optBoolean("ipv6", ipv6)
|
||||||
|
metered = json.optBoolean("metered", metered)
|
||||||
|
json.optJSONObject("proxy_apps")?.also {
|
||||||
|
proxyApps = it.optBoolean("enabled", proxyApps)
|
||||||
|
bypass = it.optBoolean("bypass", bypass)
|
||||||
|
individual = it.optJSONArray("android_list")?.asIterable()?.joinToString("\n") ?: individual
|
||||||
|
}
|
||||||
|
udpdns = json.optBoolean("udpdns", udpdns)
|
||||||
|
json.optJSONObject("udp_fallback")?.let { tryParse(it, true) }?.also { fallbackMap[this] = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun process(json: Any) {
|
||||||
|
when (json) {
|
||||||
|
is JSONObject -> {
|
||||||
|
val profile = tryParse(json)
|
||||||
|
if (profile != null) add(profile) else for (key in json.keys()) process(json.get(key))
|
||||||
|
}
|
||||||
|
is JSONArray -> json.asIterable().forEach(this::process)
|
||||||
|
// ignore other types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun finalize(create: (Profile) -> Unit) {
|
||||||
|
val profiles = ProfileManager.getAllProfiles() ?: emptyList()
|
||||||
|
for ((profile, fallback) in fallbackMap) {
|
||||||
|
val match = profiles.firstOrNull {
|
||||||
|
fallback.host == it.host && fallback.remotePort == it.remotePort &&
|
||||||
|
fallback.password == it.password && fallback.method == it.method &&
|
||||||
|
it.plugin.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
profile.udpFallback = if (match == null) {
|
||||||
|
create(fallback)
|
||||||
|
fallback.id
|
||||||
|
} else match.id
|
||||||
|
ProfileManager.updateProfile(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun parseJson(json: String, feature: Profile? = null, create: (Profile) -> Unit) = JsonParser(feature).run {
|
||||||
|
process(JSONTokener(json).nextValue())
|
||||||
|
for (profile in this) create(profile)
|
||||||
|
finalize(create)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.room.Dao
|
||||||
|
interface Dao {
|
||||||
|
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
|
||||||
|
operator fun get(id: Long): Profile?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
|
||||||
|
fun list(): List<Profile>
|
||||||
|
|
||||||
|
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
|
||||||
|
fun nextOrder(): Long?
|
||||||
|
|
||||||
|
@Query("SELECT 1 FROM `Profile` LIMIT 1")
|
||||||
|
fun isNotEmpty(): Boolean
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun create(value: Profile): Long
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(value: Profile): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM `Profile` WHERE `id` = :id")
|
||||||
|
fun delete(id: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM `Profile`")
|
||||||
|
fun deleteAll(): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
|
||||||
|
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
|
||||||
|
|
||||||
|
fun copyFeatureSettingsTo(profile: Profile) {
|
||||||
|
profile.route = route
|
||||||
|
profile.ipv6 = ipv6
|
||||||
|
profile.metered = metered
|
||||||
|
profile.proxyApps = proxyApps
|
||||||
|
profile.bypass = bypass
|
||||||
|
profile.individual = individual
|
||||||
|
profile.udpdns = udpdns
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(): Uri {
|
||||||
|
val auth = Base64.encodeToString("$method:$password".toByteArray(),
|
||||||
|
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
|
||||||
|
val wrappedHost = if (host.contains(':')) "[$host]" else host
|
||||||
|
val builder = Uri.Builder()
|
||||||
|
.scheme("ss")
|
||||||
|
.encodedAuthority("$auth@$wrappedHost:$remotePort")
|
||||||
|
val configuration = PluginConfiguration(plugin ?: "")
|
||||||
|
if (configuration.selected.isNotEmpty())
|
||||||
|
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
|
||||||
|
if (!name.isNullOrEmpty()) builder.fragment(name)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
override fun toString() = toUri().toString()
|
||||||
|
|
||||||
|
fun toJson(profiles: LongSparseArray<Profile>? = null): JSONObject = JSONObject().apply {
|
||||||
|
put("server", host)
|
||||||
|
put("server_port", remotePort)
|
||||||
|
put("password", password)
|
||||||
|
put("method", method)
|
||||||
|
if (profiles == null) return@apply
|
||||||
|
PluginConfiguration(plugin ?: "").selectedOptions.also {
|
||||||
|
if (it.id.isNotEmpty()) {
|
||||||
|
put("plugin", it.id)
|
||||||
|
put("plugin_opts", it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("remarks", name)
|
||||||
|
put("route", route)
|
||||||
|
put("remote_dns", remoteDns)
|
||||||
|
put("ipv6", ipv6)
|
||||||
|
put("metered", metered)
|
||||||
|
put("proxy_apps", JSONObject().apply {
|
||||||
|
put("enabled", proxyApps)
|
||||||
|
if (proxyApps) {
|
||||||
|
put("bypass", bypass)
|
||||||
|
// android_ prefix is used because package names are Android specific
|
||||||
|
put("android_list", JSONArray(individual.split("\n")))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
put("udpdns", udpdns)
|
||||||
|
val fallback = profiles.get(udpFallback ?: return@apply)
|
||||||
|
if (fallback != null && fallback.plugin.isNullOrEmpty()) fallback.toJson().also { put("udp_fallback", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serialize() {
|
||||||
|
DataStore.editingId = id
|
||||||
|
DataStore.privateStore.putString(Key.name, name)
|
||||||
|
DataStore.privateStore.putString(Key.host, host)
|
||||||
|
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
|
||||||
|
DataStore.privateStore.putString(Key.password, password)
|
||||||
|
DataStore.privateStore.putString(Key.route, route)
|
||||||
|
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
|
||||||
|
DataStore.privateStore.putString(Key.method, method)
|
||||||
|
DataStore.proxyApps = proxyApps
|
||||||
|
DataStore.bypass = bypass
|
||||||
|
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
|
||||||
|
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
|
||||||
|
DataStore.privateStore.putBoolean(Key.metered, metered)
|
||||||
|
DataStore.individual = individual
|
||||||
|
DataStore.plugin = plugin ?: ""
|
||||||
|
DataStore.udpFallback = udpFallback
|
||||||
|
DataStore.privateStore.remove(Key.dirty)
|
||||||
|
}
|
||||||
|
fun deserialize() {
|
||||||
|
check(id == 0L || DataStore.editingId == id)
|
||||||
|
DataStore.editingId = null
|
||||||
|
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
|
||||||
|
name = DataStore.privateStore.getString(Key.name) ?: ""
|
||||||
|
host = DataStore.privateStore.getString(Key.host) ?: ""
|
||||||
|
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
|
||||||
|
password = DataStore.privateStore.getString(Key.password) ?: ""
|
||||||
|
method = DataStore.privateStore.getString(Key.method) ?: ""
|
||||||
|
route = DataStore.privateStore.getString(Key.route) ?: ""
|
||||||
|
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
|
||||||
|
proxyApps = DataStore.proxyApps
|
||||||
|
bypass = DataStore.bypass
|
||||||
|
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
|
||||||
|
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
|
||||||
|
metered = DataStore.privateStore.getBoolean(Key.metered, false)
|
||||||
|
individual = DataStore.individual
|
||||||
|
plugin = DataStore.plugin
|
||||||
|
udpFallback = DataStore.udpFallback
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,16 +18,14 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.database
|
package org.amnezia.vpn.shadowsocks.core.database
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteCantOpenDatabaseException
|
import android.database.sqlite.SQLiteCantOpenDatabaseException
|
||||||
import android.util.LongSparseArray
|
import android.util.LongSparseArray
|
||||||
import com.github.shadowsocks.Core
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import com.github.shadowsocks.preference.DataStore
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
import com.github.shadowsocks.utils.DirectBoot
|
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||||
import com.github.shadowsocks.utils.forEachTry
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
import com.github.shadowsocks.utils.printLog
|
|
||||||
import com.google.gson.JsonStreamParser
|
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -43,7 +41,6 @@ object ProfileManager {
|
||||||
fun onRemove(profileId: Long)
|
fun onRemove(profileId: Long)
|
||||||
fun onCleared()
|
fun onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
var listener: Listener? = null
|
var listener: Listener? = null
|
||||||
|
|
||||||
@Throws(SQLException::class)
|
@Throws(SQLException::class)
|
||||||
|
@ -61,8 +58,9 @@ object ProfileManager {
|
||||||
profiles?.values?.singleOrNull { it.id == DataStore.profileId }
|
profiles?.values?.singleOrNull { it.id == DataStore.profileId }
|
||||||
} else Core.currentProfile?.first
|
} else Core.currentProfile?.first
|
||||||
val lazyClear = lazy { clear() }
|
val lazyClear = lazy { clear() }
|
||||||
jsons.asIterable().forEachTry { json ->
|
var result: Exception? = null
|
||||||
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) {
|
for (json in jsons) try {
|
||||||
|
Profile.parseJson(json.bufferedReader().readText(), feature) {
|
||||||
if (replace) {
|
if (replace) {
|
||||||
lazyClear.value
|
lazyClear.value
|
||||||
// if two profiles has the same address, treat them as the same profile and copy stats over
|
// if two profiles has the same address, treat them as the same profile and copy stats over
|
||||||
|
@ -73,9 +71,11 @@ object ProfileManager {
|
||||||
}
|
}
|
||||||
createProfile(it)
|
createProfile(it)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (result == null) result = e else result.addSuppressed(e)
|
||||||
}
|
}
|
||||||
|
if (result != null) throw result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
|
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
|
||||||
if (profiles == null) return null
|
if (profiles == null) return null
|
||||||
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
|
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
|
||||||
|
@ -99,7 +99,7 @@ object ProfileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun expand(profile: Profile): Pair<Profile, Profile?> = Pair(profile, null)
|
fun expand(profile: Profile): Pair<Profile, Profile?> = Pair(profile, profile.udpFallback?.let { getProfile(it) })
|
||||||
|
|
||||||
@Throws(SQLException::class)
|
@Throws(SQLException::class)
|
||||||
fun delProfile(id: Long) {
|
fun delProfile(id: Long) {
|
|
@ -18,39 +18,33 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.database
|
package org.amnezia.vpn.shadowsocks.core.database
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import com.github.shadowsocks.Core
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
|
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
|
||||||
import com.github.shadowsocks.utils.Key
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Database(entities = [KeyValuePair::class], version = 3)
|
@Database(entities = [KeyValuePair::class], version = 4)
|
||||||
abstract class PublicDatabase : RoomDatabase() {
|
abstract class PublicDatabase : RoomDatabase() {
|
||||||
companion object {
|
companion object {
|
||||||
private val instance by lazy {
|
private val instance by lazy {
|
||||||
Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
|
Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
|
||||||
.apply {
|
.allowMainThreadQueries()
|
||||||
addMigrations(Migration3)
|
.addMigrations(
|
||||||
allowMainThreadQueries()
|
Migration3
|
||||||
enableMultiInstanceInvalidation()
|
)
|
||||||
fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
setQueryExecutor { GlobalScope.launch { it.run() } }
|
.build()
|
||||||
}.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val kvPairDao get() = instance.keyValuePairDao()
|
val kvPairDao get() = instance.keyValuePairDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||||
|
|
||||||
internal object Migration3 : RecreateSchemaMigration(2,
|
internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
|
||||||
3,
|
|
||||||
"KeyValuePair",
|
|
||||||
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||||
"`key`, `valueType`, `value`")
|
"`key`, `valueType`, `value`")
|
||||||
}
|
}
|
|
@ -18,14 +18,14 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.database.migration
|
package org.amnezia.vpn.shadowsocks.core.database.migration
|
||||||
|
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
|
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
|
||||||
private val schema: String, private val keys: String)
|
private val schema: String, private val keys: String) :
|
||||||
: Migration(oldVersion, newVersion) {
|
Migration(oldVersion, newVersion) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE `tmp` $schema")
|
database.execSQL("CREATE TABLE `tmp` $schema")
|
||||||
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")
|
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")
|
|
@ -18,10 +18,10 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.github.shadowsocks.utils.printLog
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.sendBlocking
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
|
@ -86,13 +86,11 @@ class ChannelMonitor : Thread("ChannelMonitor") {
|
||||||
return registration.result.await()
|
return registration.result.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun wait(channel: SelectableChannel, ops: Int) =
|
suspend fun wait(channel: SelectableChannel, ops: Int) = CompletableDeferred<SelectionKey>().run {
|
||||||
CompletableDeferred<SelectionKey>().run {
|
|
||||||
register(channel, ops) {
|
register(channel, ops) {
|
||||||
if (it.isValid) try {
|
if (it.isValid) try {
|
||||||
it.interestOps(0) // stop listening
|
it.interestOps(0) // stop listening
|
||||||
} catch (_: CancelledKeyException) {
|
} catch (_: CancelledKeyException) { }
|
||||||
}
|
|
||||||
complete(it)
|
complete(it)
|
||||||
}
|
}
|
||||||
await()
|
await()
|
|
@ -18,17 +18,16 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import android.net.LocalSocket
|
import android.net.LocalSocket
|
||||||
import com.github.shadowsocks.utils.printLog
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) :
|
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
|
||||||
LocalSocketListener(name, socketFile), CoroutineScope {
|
CoroutineScope {
|
||||||
override val coroutineContext =
|
override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||||
Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
|
||||||
|
|
||||||
override fun accept(socket: LocalSocket) {
|
override fun accept(socket: LocalSocket) {
|
||||||
launch { super.accept(socket) }
|
launch { super.accept(socket) }
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
|
@ -26,12 +26,11 @@ import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.github.shadowsocks.Core
|
import androidx.core.content.getSystemService
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.actor
|
import kotlinx.coroutines.channels.actor
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
object DefaultNetworkListener {
|
object DefaultNetworkListener {
|
||||||
|
@ -40,14 +39,13 @@ object DefaultNetworkListener {
|
||||||
class Get : NetworkMessage() {
|
class Get : NetworkMessage() {
|
||||||
val response = CompletableDeferred<Network>()
|
val response = CompletableDeferred<Network>()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Stop(val key: Any) : NetworkMessage()
|
class Stop(val key: Any) : NetworkMessage()
|
||||||
|
|
||||||
class Put(val network: Network) : NetworkMessage()
|
class Put(val network: Network) : NetworkMessage()
|
||||||
class Update(val network: Network) : NetworkMessage()
|
class Update(val network: Network) : NetworkMessage()
|
||||||
class Lost(val network: Network) : NetworkMessage()
|
class Lost(val network: Network) : NetworkMessage()
|
||||||
}
|
}
|
||||||
|
@ObsoleteCoroutinesApi
|
||||||
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
|
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
|
||||||
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
||||||
var network: Network? = null
|
var network: Network? = null
|
||||||
|
@ -74,9 +72,7 @@ object DefaultNetworkListener {
|
||||||
pendingRequests.clear()
|
pendingRequests.clear()
|
||||||
listeners.values.forEach { it(network) }
|
listeners.values.forEach { it(network) }
|
||||||
}
|
}
|
||||||
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach {
|
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach { it(network) }
|
||||||
it(network)
|
|
||||||
}
|
|
||||||
is NetworkMessage.Lost -> if (network == message.network) {
|
is NetworkMessage.Lost -> if (network == message.network) {
|
||||||
network = null
|
network = null
|
||||||
listeners.values.forEach { it(null) }
|
listeners.values.forEach { it(null) }
|
||||||
|
@ -84,39 +80,55 @@ object DefaultNetworkListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun start(key: Any, listener: (Network?) -> Unit) =
|
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(DefaultNetworkListener.NetworkMessage.Start(key, listener))
|
||||||
networkActor.send(NetworkMessage.Start(key, listener))
|
|
||||||
|
|
||||||
suspend fun get() = if (fallback) @TargetApi(23) {
|
suspend fun get() = if (fallback) @TargetApi(23) {
|
||||||
Core.connectivity.activeNetwork
|
connectivity.activeNetwork ?: throw UnknownHostException() // failed to listen, return current if available
|
||||||
?: throw UnknownHostException() // failed to listen, return current if available
|
} else DefaultNetworkListener.NetworkMessage.Get().run {
|
||||||
} else NetworkMessage.Get().run {
|
|
||||||
networkActor.send(this)
|
networkActor.send(this)
|
||||||
response.await()
|
response.await()
|
||||||
}
|
}
|
||||||
|
suspend fun stop(key: Any) = networkActor.send(DefaultNetworkListener.NetworkMessage.Stop(key))
|
||||||
|
|
||||||
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
|
|
||||||
|
private object Callback: ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
super.onAvailable(network)
|
||||||
|
runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(
|
||||||
|
network: Network,
|
||||||
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) {
|
||||||
|
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||||
|
// it's a good idea to refresh capabilities
|
||||||
|
runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Update(network)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
super.onLost(network)
|
||||||
|
runBlocking {
|
||||||
|
networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
||||||
// private object Callback : ConnectivityManager.NetworkCallback() {
|
// private object Callback : ConnectivityManager.NetworkCallback() {
|
||||||
// override fun onAvailable(network: Network) =
|
// override fun onAvailable(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
|
||||||
// runBlocking { networkActor.send(NetworkMessage.Put(network)) }
|
|
||||||
//
|
|
||||||
// override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
// override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
||||||
// // it's a good idea to refresh capabilities
|
// // it's a good idea to refresh capabilities
|
||||||
// runBlocking { networkActor.send(NetworkMessage.Update(network)) }
|
// runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Update(network)) }
|
||||||
// }
|
// }
|
||||||
//
|
// override fun onLost(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network)) }
|
||||||
// override fun onLost(network: Network) =
|
|
||||||
// runBlocking { networkActor.send(NetworkMessage.Lost(network)) }
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
private var fallback = false
|
private var fallback = false
|
||||||
|
private val connectivity = app.getSystemService<ConnectivityManager>()!!
|
||||||
private val request = NetworkRequest.Builder().apply {
|
private val request = NetworkRequest.Builder().apply {
|
||||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||||
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||||
|
@ -128,18 +140,15 @@ object DefaultNetworkListener {
|
||||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||||
*/
|
*/
|
||||||
private fun register() {
|
private fun register() {
|
||||||
// if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
||||||
// Core.connectivity.registerDefaultNetworkCallback(Callback)
|
connectivity.registerDefaultNetworkCallback(Callback)
|
||||||
// } else try {
|
} else try {
|
||||||
// fallback = false
|
fallback = false
|
||||||
// // we want REQUEST here instead of LISTEN
|
// we want REQUEST here instead of LISTEN
|
||||||
// Core.connectivity.requestNetwork(request, Callback)
|
connectivity.requestNetwork(request, Callback)
|
||||||
// } catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
// // known bug: https://stackoverflow.com/a/33509180/2245107
|
fallback = true
|
||||||
// // if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e)
|
|
||||||
// fallback = true
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun unregister() {}//= Core.connectivity.unregisterNetworkCallback(Callback)
|
private fun unregister() = connectivity.unregisterNetworkCallback(Callback)
|
||||||
}
|
}
|
|
@ -18,21 +18,23 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.github.shadowsocks.Core.app
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||||
import org.amnezia.vpn.R
|
import org.amnezia.vpn.R
|
||||||
import com.github.shadowsocks.utils.useCancellable
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
import kotlinx.coroutines.GlobalScope
|
import org.amnezia.vpn.shadowsocks.core.utils.disconnectFromMain
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.Proxy
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
@ -42,21 +44,17 @@ import java.net.URLConnection
|
||||||
class HttpsTest : ViewModel() {
|
class HttpsTest : ViewModel() {
|
||||||
sealed class Status {
|
sealed class Status {
|
||||||
protected abstract val status: CharSequence
|
protected abstract val status: CharSequence
|
||||||
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) =
|
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) = setStatus(status)
|
||||||
setStatus(status)
|
|
||||||
|
|
||||||
object Idle : Status() {
|
object Idle : Status() {
|
||||||
override val status get() = app.getText(R.string.vpn_connected)
|
override val status get() = app.getText(R.string.vpn_connected)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Testing : Status() {
|
object Testing : Status() {
|
||||||
override val status get() = app.getText(R.string.connection_test_testing)
|
override val status get() = app.getText(R.string.connection_test_testing)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Success(private val elapsed: Long) : Status() {
|
class Success(private val elapsed: Long) : Status() {
|
||||||
override val status get() = app.getString(R.string.connection_test_available, elapsed)
|
override val status get() = app.getString(R.string.connection_test_available, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Error : Status() {
|
sealed class Error : Status() {
|
||||||
override val status get() = app.getText(R.string.connection_test_fail)
|
override val status get() = app.getText(R.string.connection_test_fail)
|
||||||
protected abstract val error: String
|
protected abstract val error: String
|
||||||
|
@ -71,43 +69,48 @@ class HttpsTest : ViewModel() {
|
||||||
class UnexpectedResponseCode(private val code: Int) : Error() {
|
class UnexpectedResponseCode(private val code: Int) : Error() {
|
||||||
override val error get() = app.getString(R.string.connection_test_error_status_code, code)
|
override val error get() = app.getString(R.string.connection_test_error_status_code, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
class IOFailure(private val e: IOException) : Error() {
|
class IOFailure(private val e: IOException) : Error() {
|
||||||
override val error get() = app.getString(R.string.connection_test_error, e.message)
|
override val error get() = app.getString(R.string.connection_test_error, e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var running: Job? = null
|
private var running: Pair<HttpURLConnection, Job>? = null
|
||||||
val status = MutableLiveData<Status>().apply { value = Status.Idle }
|
val status = MutableLiveData<Status>().apply { value = Status.Idle }
|
||||||
|
|
||||||
fun testConnection() {
|
fun testConnection() {
|
||||||
cancelTest()
|
cancelTest()
|
||||||
status.value = Status.Testing
|
status.value = Status.Testing
|
||||||
val url = URL("https", "www.google.com", "/generate_204")
|
val url = URL("https", when ((Core.currentProfile ?: return).first.route) {
|
||||||
val conn = (url.openConnection()) as HttpURLConnection
|
Acl.CHINALIST -> "www.qualcomm.cn"
|
||||||
|
else -> "www.google.com"
|
||||||
|
}, "/generate_204")
|
||||||
|
val conn = (if (DataStore.serviceMode != Key.modeVpn) {
|
||||||
|
url.openConnection(Proxy(Proxy.Type.SOCKS, DataStore.proxyAddress))
|
||||||
|
} else url.openConnection()) as HttpURLConnection
|
||||||
conn.setRequestProperty("Connection", "close")
|
conn.setRequestProperty("Connection", "close")
|
||||||
conn.instanceFollowRedirects = false
|
conn.instanceFollowRedirects = false
|
||||||
conn.useCaches = false
|
conn.useCaches = false
|
||||||
running = GlobalScope.launch(Dispatchers.Main.immediate) {
|
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
status.value = conn.useCancellable {
|
status.value = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
val code = responseCode
|
val code = conn.responseCode
|
||||||
val elapsed = SystemClock.elapsedRealtime() - start
|
val elapsed = SystemClock.elapsedRealtime() - start
|
||||||
if (code == 204 || code == 200 && responseLength == 0L) Status.Success(elapsed)
|
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed)
|
||||||
else Status.Error.UnexpectedResponseCode(code)
|
else Status.Error.UnexpectedResponseCode(code)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Status.Error.IOFailure(e)
|
Status.Error.IOFailure(e)
|
||||||
} finally {
|
} finally {
|
||||||
disconnect()
|
conn.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelTest() {
|
private fun cancelTest() = running?.let { (conn, job) ->
|
||||||
running?.cancel()
|
job.cancel() // ensure job is cancelled before interrupting
|
||||||
|
conn.disconnectFromMain()
|
||||||
running = null
|
running = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,9 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import com.github.shadowsocks.bg.BaseService
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
import com.github.shadowsocks.utils.printLog
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.xbill.DNS.*
|
import org.xbill.DNS.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -30,6 +29,7 @@ import java.nio.ByteBuffer
|
||||||
import java.nio.channels.DatagramChannel
|
import java.nio.channels.DatagramChannel
|
||||||
import java.nio.channels.SelectionKey
|
import java.nio.channels.SelectionKey
|
||||||
import java.nio.channels.SocketChannel
|
import java.nio.channels.SocketChannel
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple DNS conditional forwarder.
|
* A simple DNS conditional forwarder.
|
||||||
|
@ -41,9 +41,7 @@ import java.nio.channels.SocketChannel
|
||||||
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
|
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
|
||||||
*/
|
*/
|
||||||
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
|
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
|
||||||
private val remoteDns: Socks5Endpoint,
|
private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope {
|
||||||
private val proxy: SocketAddress,
|
|
||||||
private val hosts: HostsFile) : CoroutineScope {
|
|
||||||
/**
|
/**
|
||||||
* Forward all requests to remote and ignore localResolver.
|
* Forward all requests to remote and ignore localResolver.
|
||||||
*/
|
*/
|
||||||
|
@ -70,30 +68,14 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
|
||||||
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
|
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
|
||||||
request.question?.also { addRecord(it, Section.QUESTION) }
|
request.question?.also { addRecord(it, Section.QUESTION) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cookDnsResponse(request: Message, results: Iterable<InetAddress>) =
|
|
||||||
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
|
||||||
header.setFlag(Flags.RA.toInt()) // recursion available
|
|
||||||
for (address in results) addRecord(when (address) {
|
|
||||||
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
|
|
||||||
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
|
|
||||||
else -> error("Unsupported address $address")
|
|
||||||
}, Section.ANSWER)
|
|
||||||
}.toWire())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val monitor = ChannelMonitor()
|
private val monitor = ChannelMonitor()
|
||||||
|
|
||||||
override val coroutineContext =
|
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||||
SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
|
||||||
|
|
||||||
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
|
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
|
||||||
configureBlocking(false)
|
configureBlocking(false)
|
||||||
try {
|
|
||||||
socket().bind(listen)
|
socket().bind(listen)
|
||||||
} catch (e: BindException) {
|
|
||||||
throw BaseService.ExpectedExceptionWrapper(e)
|
|
||||||
}
|
|
||||||
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
|
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,27 +93,20 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
|
||||||
val request = try {
|
val request = try {
|
||||||
Message(packet)
|
Message(packet)
|
||||||
} catch (e: IOException) { // we cannot parse the message, do not attempt to handle it at all
|
} catch (e: IOException) { // we cannot parse the message, do not attempt to handle it at all
|
||||||
// Crashlytics.log(Log.WARN, TAG, e.message)
|
printLog(e)
|
||||||
return forward(packet)
|
return forward(packet)
|
||||||
}
|
}
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
|
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
|
||||||
try {
|
try {
|
||||||
if (request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
|
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
|
||||||
val question = request.question
|
val question = request.question
|
||||||
if (question?.type != Type.A) return@supervisorScope remote.await()
|
if (question?.type != Type.A) return@supervisorScope remote.await()
|
||||||
val host = question.name.toString(true)
|
val host = question.name.toString(true)
|
||||||
val hostsResults = hosts.resolve(host)
|
|
||||||
if (hostsResults.isNotEmpty()) {
|
|
||||||
remote.cancel()
|
|
||||||
return@supervisorScope cookDnsResponse(request, hostsResults)
|
|
||||||
}
|
|
||||||
if (forwardOnly) return@supervisorScope remote.await()
|
|
||||||
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
|
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
|
||||||
val localResults = try {
|
val localResults = try {
|
||||||
withTimeout(TIMEOUT) { localResolver(host) }
|
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
|
||||||
} catch (_: TimeoutCancellationException) {
|
} catch (_: TimeoutCancellationException) {
|
||||||
// Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving")
|
|
||||||
return@supervisorScope remote.await()
|
return@supervisorScope remote.await()
|
||||||
} catch (_: UnknownHostException) {
|
} catch (_: UnknownHostException) {
|
||||||
return@supervisorScope remote.await()
|
return@supervisorScope remote.await()
|
||||||
|
@ -139,15 +114,19 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
|
||||||
if (localResults.isEmpty()) return@supervisorScope remote.await()
|
if (localResults.isEmpty()) return@supervisorScope remote.await()
|
||||||
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
|
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
|
||||||
remote.cancel()
|
remote.cancel()
|
||||||
cookDnsResponse(request, localResults.asIterable())
|
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
||||||
|
header.setFlag(Flags.RA.toInt()) // recursion available
|
||||||
|
for (address in localResults) addRecord(when (address) {
|
||||||
|
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
|
||||||
|
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
|
||||||
|
else -> throw IllegalStateException("Unsupported address $address")
|
||||||
|
}, Section.ANSWER)
|
||||||
|
}.toWire())
|
||||||
} else remote.await()
|
} else remote.await()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
remote.cancel()
|
remote.cancel()
|
||||||
when (e) {
|
when (e) {
|
||||||
// is TimeoutCancellationException -> Crashlytics.log(Log.WARN, TAG, "Remote resolving timed out")
|
is CancellationException -> { } // ignore
|
||||||
is CancellationException -> {
|
|
||||||
} // ignore
|
|
||||||
// is IOException -> Crashlytics.log(Log.WARN, TAG, e.message)
|
|
||||||
else -> printLog(e)
|
else -> printLog(e)
|
||||||
}
|
}
|
||||||
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
||||||
|
@ -157,6 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalUnsignedTypes
|
||||||
private suspend fun forward(packet: ByteBuffer): ByteBuffer {
|
private suspend fun forward(packet: ByteBuffer): ByteBuffer {
|
||||||
packet.position(0) // the packet might have been parsed, reset to beginning
|
packet.position(0) // the packet might have been parsed, reset to beginning
|
||||||
return if (tcp) SocketChannel.open().use { channel ->
|
return if (tcp) SocketChannel.open().use { channel ->
|
||||||
|
@ -166,9 +146,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
|
||||||
while (!channel.finishConnect()) monitor.wait(channel, SelectionKey.OP_CONNECT)
|
while (!channel.finishConnect()) monitor.wait(channel, SelectionKey.OP_CONNECT)
|
||||||
while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE)
|
while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE)
|
||||||
val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE)
|
val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE)
|
||||||
remoteDns.tcpUnwrap(result, channel::read) {
|
remoteDns.tcpUnwrap(result, channel::read) { monitor.wait(channel, SelectionKey.OP_READ) }
|
||||||
monitor.wait(channel, SelectionKey.OP_READ)
|
|
||||||
}
|
|
||||||
result
|
result
|
||||||
} else DatagramChannel.open().use { channel ->
|
} else DatagramChannel.open().use { channel ->
|
||||||
channel.configureBlocking(false)
|
channel.configureBlocking(false)
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import android.net.LocalServerSocket
|
import android.net.LocalServerSocket
|
||||||
import android.net.LocalSocket
|
import android.net.LocalSocket
|
||||||
|
@ -26,7 +26,7 @@ import android.net.LocalSocketAddress
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import com.github.shadowsocks.utils.printLog
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.sendBlocking
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
|
@ -48,7 +48,6 @@ abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name
|
||||||
* Inherited class do not need to close input/output streams as they will be closed automatically.
|
* Inherited class do not need to close input/output streams as they will be closed automatically.
|
||||||
*/
|
*/
|
||||||
protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
|
protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
|
||||||
|
|
||||||
protected abstract fun acceptInternal(socket: LocalSocket)
|
protected abstract fun acceptInternal(socket: LocalSocket)
|
||||||
final override fun run() {
|
final override fun run() {
|
||||||
localSocket.use {
|
localSocket.use {
|
|
@ -18,9 +18,9 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import com.github.shadowsocks.utils.parseNumericAddress
|
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
|
||||||
import net.sourceforge.jsocks.Socks4Message
|
import net.sourceforge.jsocks.Socks4Message
|
||||||
import net.sourceforge.jsocks.Socks5Message
|
import net.sourceforge.jsocks.Socks5Message
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
|
@ -32,13 +32,12 @@ import kotlin.math.max
|
||||||
|
|
||||||
class Socks5Endpoint(host: String, port: Int) {
|
class Socks5Endpoint(host: String, port: Int) {
|
||||||
private val dest = host.parseNumericAddress().let { numeric ->
|
private val dest = host.parseNumericAddress().let { numeric ->
|
||||||
val bytes = numeric?.address
|
val bytes = numeric?.address ?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
|
||||||
?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
|
|
||||||
val type = when (numeric) {
|
val type = when (numeric) {
|
||||||
null -> Socks5Message.SOCKS_ATYP_DOMAINNAME
|
null -> Socks5Message.SOCKS_ATYP_DOMAINNAME
|
||||||
is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4
|
is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4
|
||||||
is Inet6Address -> Socks5Message.SOCKS_ATYP_IPV6
|
is Inet6Address -> Socks5Message.SOCKS_ATYP_IPV6
|
||||||
else -> error("Unsupported address type $numeric")
|
else -> throw IllegalStateException("Unsupported address type")
|
||||||
}
|
}
|
||||||
ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply {
|
ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply {
|
||||||
put(type.toByte())
|
put(type.toByte())
|
||||||
|
@ -66,35 +65,34 @@ class Socks5Endpoint(host: String, port: Int) {
|
||||||
flip()
|
flip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
|
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
|
||||||
|
@ExperimentalUnsignedTypes
|
||||||
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
|
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
|
||||||
suspend fun readBytes(till: Int) {
|
suspend fun readBytes(till: Int) {
|
||||||
if (buffer.position() >= till) return
|
if (buffer.position() >= till) return
|
||||||
while (reader(buffer) >= 0 && buffer.position() < till) wait()
|
while (reader(buffer) >= 0 && buffer.position() < till) wait()
|
||||||
if (buffer.position() < till) throw EOFException("${buffer.position()} < $till")
|
if (buffer.position() < till) throw EOFException("${buffer.position()} < $till")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun read(index: Int): Byte {
|
suspend fun read(index: Int): Byte {
|
||||||
readBytes(index + 1)
|
readBytes(index + 1)
|
||||||
return buffer[index]
|
return buffer[index]
|
||||||
}
|
}
|
||||||
if (read(0) != Socks5Message.SOCKS_VERSION.toByte()) throw IOException("Unsupported SOCKS version ${buffer[0]}")
|
check(read(0) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
|
||||||
if (read(1) != 0.toByte()) throw IOException("Unsupported authentication ${buffer[1]}")
|
if (read(1) != 0.toByte()) throw IOException("Unsupported authentication ${buffer[1]}")
|
||||||
if (read(2) != Socks5Message.SOCKS_VERSION.toByte()) throw IOException("Unsupported SOCKS version ${buffer[2]}")
|
check(read(2) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
|
||||||
if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}")
|
if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}")
|
||||||
val dataOffset = when (val type = read(5)) {
|
val dataOffset = when (read(5)) {
|
||||||
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
||||||
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
|
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
|
||||||
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
||||||
else -> throw IOException("Unsupported address type $type")
|
else -> throw IllegalStateException("Unsupported address type ${buffer[5]}")
|
||||||
} + 8
|
} + 8
|
||||||
readBytes(dataOffset + 2)
|
readBytes(dataOffset + 2)
|
||||||
buffer.limit(buffer.position()) // store old position to update mark
|
buffer.limit(buffer.position()) // store old position to update mark
|
||||||
buffer.position(dataOffset)
|
buffer.position(dataOffset)
|
||||||
val dataLength = buffer.short.toUShort().toInt()
|
val dataLength = buffer.short.toUShort().toInt()
|
||||||
val end = buffer.position() + dataLength
|
val end = buffer.position() + dataLength
|
||||||
if (end > buffer.capacity()) throw IOException("Buffer too small to contain the message: $dataLength > ${buffer.capacity() - buffer.position()}")
|
check(end <= buffer.capacity()) { "Buffer too small to contain the message" }
|
||||||
buffer.mark()
|
buffer.mark()
|
||||||
buffer.position(buffer.limit()) // restore old position
|
buffer.position(buffer.limit()) // restore old position
|
||||||
buffer.limit(end)
|
buffer.limit(end)
|
||||||
|
@ -102,13 +100,7 @@ class Socks5Endpoint(host: String, port: Int) {
|
||||||
buffer.reset()
|
buffer.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ByteBuffer.tryPosition(newPosition: Int) {
|
fun udpWrap(packet: ByteBuffer) = ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
|
||||||
if (limit() < newPosition) throw EOFException("${limit()} < $newPosition")
|
|
||||||
position(newPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun udpWrap(packet: ByteBuffer) =
|
|
||||||
ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
|
|
||||||
// header
|
// header
|
||||||
putShort(0) // reserved
|
putShort(0) // reserved
|
||||||
put(0) // fragment number
|
put(0) // fragment number
|
||||||
|
@ -117,15 +109,14 @@ class Socks5Endpoint(host: String, port: Int) {
|
||||||
put(packet)
|
put(packet)
|
||||||
flip()
|
flip()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
|
fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
|
||||||
fun udpUnwrap(packet: ByteBuffer) {
|
fun udpUnwrap(packet: ByteBuffer) {
|
||||||
packet.tryPosition(3)
|
packet.position(3)
|
||||||
packet.tryPosition(6 + when (val type = packet.get()) {
|
packet.position(6 + when (packet.get()) {
|
||||||
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
||||||
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
|
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
|
||||||
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
||||||
else -> throw IOException("Unsupported address type $type")
|
else -> throw IllegalStateException("Unsupported address type")
|
||||||
})
|
})
|
||||||
packet.mark()
|
packet.mark()
|
||||||
}
|
}
|
|
@ -18,9 +18,9 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import com.github.shadowsocks.utils.parseNumericAddress
|
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
|
||||||
private val addressLength get() = address.address.size shl 3
|
private val addressLength get() = address.address.size shl 3
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(prefixSize in 0..addressLength) { "prefixSize $prefixSize not in 0..$addressLength" }
|
if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException("prefixSize: $prefixSize")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun matches(other: InetAddress): Boolean {
|
fun matches(other: InetAddress): Boolean {
|
||||||
|
@ -80,6 +80,5 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
|
||||||
val that = other as? Subnet
|
val that = other as? Subnet
|
||||||
return address == that?.address && prefixSize == that.prefixSize
|
return address == that?.address && prefixSize == that.prefixSize
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int = Objects.hash(address, prefixSize)
|
override fun hashCode(): Int = Objects.hash(address, prefixSize)
|
||||||
}
|
}
|
|
@ -18,9 +18,9 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.net
|
||||||
|
|
||||||
import com.github.shadowsocks.utils.readableMessage
|
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -34,8 +34,7 @@ object TcpFastOpen {
|
||||||
*/
|
*/
|
||||||
val supported by lazy {
|
val supported by lazy {
|
||||||
if (File(PATH).canRead()) return@lazy true
|
if (File(PATH).canRead()) return@lazy true
|
||||||
val match =
|
val match = """^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
|
||||||
"""^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
|
|
||||||
if (match == null) false else when (match.groupValues[1].toInt()) {
|
if (match == null) false else when (match.groupValues[1].toInt()) {
|
||||||
in Int.MIN_VALUE..2 -> false
|
in Int.MIN_VALUE..2 -> false
|
||||||
3 -> when (match.groupValues[2].toInt()) {
|
3 -> when (match.groupValues[2].toInt()) {
|
||||||
|
@ -47,8 +46,7 @@ object TcpFastOpen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sendEnabled: Boolean
|
val sendEnabled: Boolean get() {
|
||||||
get() {
|
|
||||||
val file = File(PATH)
|
val file = File(PATH)
|
||||||
// File.readText doesn't work since this special file will return length 0
|
// File.readText doesn't work since this special file will return length 0
|
||||||
// on Android containers like Chrome OS, this file does not exist so we simply judge by the kernel version
|
// on Android containers like Chrome OS, this file does not exist so we simply judge by the kernel version
|
||||||
|
@ -63,6 +61,5 @@ object TcpFastOpen {
|
||||||
e.readableMessage
|
e.readableMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
|
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* *
|
* *
|
||||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
* *
|
* *
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
* it under the terms of the GNU General Public License as published by *
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
@ -18,23 +18,16 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.net
|
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||||
|
|
||||||
import com.github.shadowsocks.utils.computeIfAbsentCompat
|
import android.content.pm.ResolveInfo
|
||||||
import com.github.shadowsocks.utils.parseNumericAddress
|
import android.os.Bundle
|
||||||
import java.net.InetAddress
|
|
||||||
|
|
||||||
class HostsFile(input: String = "") {
|
|
||||||
private val map = mutableMapOf<String, MutableSet<InetAddress>>()
|
|
||||||
|
|
||||||
|
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
|
||||||
init {
|
init {
|
||||||
for (line in input.lineSequence()) {
|
check(resolveInfo.providerInfo != null)
|
||||||
val entries = line.substringBefore('#').splitToSequence(' ', '\t').filter { it.isNotEmpty() }
|
|
||||||
val address = entries.firstOrNull()?.parseNumericAddress() ?: continue
|
|
||||||
for (hostname in entries.drop(1)) map.computeIfAbsentCompat(hostname) { LinkedHashSet(1) }.add(address)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val configuredHostnames get() = map.size
|
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
|
||||||
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList()
|
override val packageName: String get() = resolveInfo.providerInfo.packageName
|
||||||
}
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||||
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
|
||||||
|
object NoPlugin : Plugin() {
|
||||||
|
override val id: String get() = ""
|
||||||
|
override val label: CharSequence get() = app.getText(R.string.plugin_disabled)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
|
abstract class Plugin {
|
||||||
|
abstract val id: String
|
||||||
|
abstract val label: CharSequence
|
||||||
|
open val icon: Drawable? get() = null
|
||||||
|
open val defaultConfig: String? get() = null
|
||||||
|
open val packageName: String get() = ""
|
||||||
|
open val trusted: Boolean get() = true
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||||
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.Commandline
|
||||||
|
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val selected: String) {
|
||||||
|
private constructor(plugins: List<PluginOptions>) : this(
|
||||||
|
plugins.filter { it.id.isNotEmpty() }.associate { it.id to it },
|
||||||
|
if (plugins.isEmpty()) "" else plugins[0].id)
|
||||||
|
constructor(plugin: String) : this(plugin.split('\n').map { line ->
|
||||||
|
if (line.startsWith("kcptun ")) {
|
||||||
|
val opt = PluginOptions()
|
||||||
|
opt.id = "kcptun"
|
||||||
|
try {
|
||||||
|
val iterator = Commandline.translateCommandline(line).drop(1).iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val option = iterator.next()
|
||||||
|
when {
|
||||||
|
option == "--nocomp" -> opt["nocomp"] = null
|
||||||
|
option.startsWith("--") -> opt[option.substring(2)] = iterator.next()
|
||||||
|
else -> throw IllegalArgumentException("Unknown kcptun parameter: $option")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
}
|
||||||
|
opt
|
||||||
|
} else PluginOptions(line)
|
||||||
|
})
|
||||||
|
|
||||||
|
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else
|
||||||
|
pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig)
|
||||||
|
val selectedOptions: PluginOptions get() = getOptions(selected)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val result = LinkedList<PluginOptions>()
|
||||||
|
for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
|
||||||
|
if (!pluginsOptions.contains(selected)) result.addFirst(selectedOptions)
|
||||||
|
return result.joinToString("\n") { it.toString(false) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.Signature
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.system.Os
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.signaturesCompat
|
||||||
|
import org.amnezia.vpn.shadowsocks.plugin.PluginContract
|
||||||
|
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
object PluginManager {
|
||||||
|
class PluginNotFoundException(private val plugin: String) : FileNotFoundException(plugin) {
|
||||||
|
override fun getLocalizedMessage() = app.getString(R.string.plugin_unknown, plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
|
||||||
|
* to publish or has published plugins for this app. You can obtain your public key by executing:
|
||||||
|
*
|
||||||
|
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
|
||||||
|
*
|
||||||
|
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
|
||||||
|
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
|
||||||
|
*/
|
||||||
|
val trustedSignatures by lazy {
|
||||||
|
Core.packageInfo.signaturesCompat.toSet() +
|
||||||
|
Signature(Base64.decode( // @Mygod
|
||||||
|
"""
|
||||||
|
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|
||||||
|
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|
||||||
|
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|
||||||
|
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|
||||||
|
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|
||||||
|
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||||
|
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|
||||||
|
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|
||||||
|
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|
||||||
|
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|
||||||
|
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|
||||||
|
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|
||||||
|
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|
||||||
|
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|
||||||
|
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|
||||||
|
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|
||||||
|
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|
||||||
|
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
|
||||||
|
""", Base64.DEFAULT)) +
|
||||||
|
Signature(Base64.decode( // @madeye
|
||||||
|
"""
|
||||||
|
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|
||||||
|
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|
||||||
|
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|
||||||
|
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|
||||||
|
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|
||||||
|
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|
||||||
|
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|
||||||
|
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|
||||||
|
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|
||||||
|
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|
||||||
|
|1iNuLIssdhDz3IL9Yg==
|
||||||
|
""", Base64.DEFAULT))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var receiver: BroadcastReceiver? = null
|
||||||
|
private var cachedPlugins: Map<String, Plugin>? = null
|
||||||
|
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
|
||||||
|
if (receiver == null) receiver = Core.listenForPackageChanges {
|
||||||
|
synchronized(this) {
|
||||||
|
receiver = null
|
||||||
|
cachedPlugins = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cachedPlugins == null) {
|
||||||
|
val pm = app.packageManager
|
||||||
|
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
|
||||||
|
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
|
||||||
|
}
|
||||||
|
cachedPlugins!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildUri(id: String) = Uri.Builder()
|
||||||
|
.scheme(PluginContract.SCHEME)
|
||||||
|
.authority(PluginContract.AUTHORITY)
|
||||||
|
.path("/$id")
|
||||||
|
.build()
|
||||||
|
fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
|
||||||
|
|
||||||
|
// the following parts are meant to be used by :bg
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun init(options: PluginOptions): String? {
|
||||||
|
if (options.id.isEmpty()) return null
|
||||||
|
var throwable: Throwable? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val path = initNative(options)
|
||||||
|
if (path != null) return path
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
if (throwable == null) throwable = t else printLog(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add other plugin types here
|
||||||
|
|
||||||
|
throw throwable ?: PluginNotFoundException(options.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNative(options: PluginOptions): String? {
|
||||||
|
val providers = app.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
|
||||||
|
if (providers.isEmpty()) return null
|
||||||
|
val uri = Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||||
|
.authority(providers.single().providerInfo.authority)
|
||||||
|
.build()
|
||||||
|
val cr = app.contentResolver
|
||||||
|
return try {
|
||||||
|
initNativeFast(cr, options, uri)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
printLog(t)
|
||||||
|
initNativeSlow(cr, options, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String {
|
||||||
|
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null,
|
||||||
|
bundleOf(Pair(PluginContract.EXTRA_OPTIONS, options.id)))!!.getString(PluginContract.EXTRA_ENTRY)!!
|
||||||
|
check(File(result).canExecute())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Recycle")
|
||||||
|
private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
|
||||||
|
var initialized = false
|
||||||
|
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
|
||||||
|
val pluginDir = File(Core.deviceStorage.noBackupFilesDir, "plugin")
|
||||||
|
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
|
||||||
|
?: return null).use { cursor ->
|
||||||
|
if (!cursor.moveToFirst()) entryNotFound()
|
||||||
|
pluginDir.deleteRecursively()
|
||||||
|
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
|
||||||
|
val pluginDirPath = pluginDir.absolutePath + '/'
|
||||||
|
do {
|
||||||
|
val path = cursor.getString(0)
|
||||||
|
val file = File(pluginDir, path)
|
||||||
|
check(file.absolutePath.startsWith(pluginDirPath))
|
||||||
|
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
|
||||||
|
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
|
||||||
|
}
|
||||||
|
Os.chmod(file.absolutePath, when (cursor.getType(1)) {
|
||||||
|
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
|
||||||
|
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
|
||||||
|
else -> throw IllegalArgumentException("File mode should be of type int")
|
||||||
|
})
|
||||||
|
if (path == options.id) initialized = true
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
if (!initialized) entryNotFound()
|
||||||
|
return File(pluginDir, options.id).absolutePath
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||||
|
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
|
import org.amnezia.vpn.shadowsocks.core.utils.signaturesCompat
|
||||||
|
import org.amnezia.vpn.shadowsocks.plugin.PluginContract
|
||||||
|
|
||||||
|
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
|
||||||
|
protected abstract val metaData: Bundle
|
||||||
|
|
||||||
|
override val id: String by lazy { metaData.getString(PluginContract.METADATA_KEY_ID)!! }
|
||||||
|
override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) }
|
||||||
|
override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) }
|
||||||
|
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG)!! }
|
||||||
|
override val packageName: String get() = resolveInfo.resolvePackageName
|
||||||
|
override val trusted by lazy {
|
||||||
|
Core.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,18 +18,20 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.preference
|
package org.amnezia.vpn.shadowsocks.core.preference
|
||||||
|
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
import com.github.shadowsocks.Core
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import com.github.shadowsocks.database.PrivateDatabase
|
import org.amnezia.vpn.shadowsocks.core.database.PrivateDatabase
|
||||||
import com.github.shadowsocks.database.PublicDatabase
|
import org.amnezia.vpn.shadowsocks.core.database.PublicDatabase
|
||||||
import com.github.shadowsocks.net.TcpFastOpen
|
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||||
import com.github.shadowsocks.utils.DirectBoot
|
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||||
import com.github.shadowsocks.utils.Key
|
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||||
import com.github.shadowsocks.utils.parsePort
|
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.net.SocketException
|
||||||
|
|
||||||
object DataStore : OnPreferenceDataStoreChangeListener {
|
object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
|
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
|
||||||
|
@ -40,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
publicStore.registerChangeListener(this)
|
publicStore.registerChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
|
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
|
||||||
when (key) {
|
when (key) {
|
||||||
Key.id -> if (directBootAware) DirectBoot.update()
|
Key.id -> if (directBootAware) DirectBoot.update()
|
||||||
}
|
}
|
||||||
|
@ -48,7 +50,6 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
|
|
||||||
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
|
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
|
||||||
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
|
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
|
||||||
|
|
||||||
private fun getLocalPort(key: String, default: Int): Int {
|
private fun getLocalPort(key: String, default: Int): Int {
|
||||||
val value = publicStore.getInt(key)
|
val value = publicStore.getInt(key)
|
||||||
return if (value != null) {
|
return if (value != null) {
|
||||||
|
@ -62,8 +63,29 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
set(value) = publicStore.putLong(Key.id, value)
|
set(value) = publicStore.putLong(Key.id, value)
|
||||||
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
|
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
|
||||||
val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
|
val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
|
||||||
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, false)
|
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, true)
|
||||||
val listenAddress get() = "127.0.0.1"
|
val serviceMode get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alternative way to detect this interface could be checking MAC address = 00:ff:aa:00:00:55, but there is no
|
||||||
|
* reliable way of getting MAC address for now.
|
||||||
|
*/
|
||||||
|
val hasArc0 by lazy {
|
||||||
|
var retry = 0
|
||||||
|
while (retry < 5) {
|
||||||
|
try {
|
||||||
|
return@lazy NetworkInterface.getByName("arc0") != null
|
||||||
|
} catch (_: SocketException) { }
|
||||||
|
retry++
|
||||||
|
Thread.sleep(100L shl retry)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Binding bogus IP address 100.115.92.2 in Chrome OS directly does not seem to work reliably. It might be due to
|
||||||
|
* the IP may not be available when the device is not connected to any network.
|
||||||
|
*/
|
||||||
|
val listenAddress get() = if (publicStore.getBoolean(Key.shareOverLan, hasArc0)) "0.0.0.0" else "127.0.0.1"
|
||||||
var portProxy: Int
|
var portProxy: Int
|
||||||
get() = getLocalPort(Key.portProxy, 1080)
|
get() = getLocalPort(Key.portProxy, 1080)
|
||||||
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
||||||
|
@ -71,6 +93,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
var portLocalDns: Int
|
var portLocalDns: Int
|
||||||
get() = getLocalPort(Key.portLocalDns, 5450)
|
get() = getLocalPort(Key.portLocalDns, 5450)
|
||||||
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
|
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
|
||||||
|
var portTransproxy: Int
|
||||||
|
get() = getLocalPort(Key.portTransproxy, 8200)
|
||||||
|
set(value) = publicStore.putString(Key.portTransproxy, value.toString())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize settings that have complicated default values.
|
* Initialize settings that have complicated default values.
|
||||||
|
@ -79,11 +104,27 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
|
if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
|
||||||
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
|
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
|
||||||
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
||||||
|
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||||
}
|
}
|
||||||
|
|
||||||
var editingId: Long?
|
var editingId: Long?
|
||||||
get() = privateStore.getLong(Key.id)
|
get() = privateStore.getLong(Key.id)
|
||||||
set(value) = privateStore.putLong(Key.id, value)
|
set(value) = privateStore.putLong(Key.id, value)
|
||||||
|
var proxyApps: Boolean
|
||||||
|
get() = privateStore.getBoolean(Key.proxyApps) ?: false
|
||||||
|
set(value) = privateStore.putBoolean(Key.proxyApps, value)
|
||||||
|
var bypass: Boolean
|
||||||
|
get() = privateStore.getBoolean(Key.bypass) ?: false
|
||||||
|
set(value) = privateStore.putBoolean(Key.bypass, value)
|
||||||
|
var individual: String
|
||||||
|
get() = privateStore.getString(Key.individual) ?: ""
|
||||||
|
set(value) = privateStore.putString(Key.individual, value)
|
||||||
|
var plugin: String
|
||||||
|
get() = privateStore.getString(Key.plugin) ?: ""
|
||||||
|
set(value) = privateStore.putString(Key.plugin, value)
|
||||||
|
var udpFallback: Long?
|
||||||
|
get() = privateStore.getLong(Key.udpFallback)
|
||||||
|
set(value) = privateStore.putLong(Key.udpFallback, value)
|
||||||
var dirty: Boolean
|
var dirty: Boolean
|
||||||
get() = privateStore.getBoolean(Key.dirty) ?: false
|
get() = privateStore.getBoolean(Key.dirty) ?: false
|
||||||
set(value) = privateStore.putBoolean(Key.dirty, value)
|
set(value) = privateStore.putBoolean(Key.dirty, value)
|
|
@ -18,10 +18,10 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.preference
|
package org.amnezia.vpn.shadowsocks.core.preference
|
||||||
|
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
interface OnPreferenceDataStoreChangeListener {
|
interface OnPreferenceDataStoreChangeListener {
|
||||||
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
|
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?)
|
||||||
}
|
}
|
|
@ -18,15 +18,14 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.preference
|
package org.amnezia.vpn.shadowsocks.core.preference
|
||||||
|
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
import com.github.shadowsocks.database.KeyValuePair
|
import org.amnezia.vpn.shadowsocks.core.database.KeyValuePair
|
||||||
import java.util.*
|
import java.util.HashSet
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||||
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() {
|
||||||
PreferenceDataStore() {
|
|
||||||
fun getBoolean(key: String) = kvPairDao[key]?.boolean
|
fun getBoolean(key: String) = kvPairDao[key]?.boolean
|
||||||
fun getFloat(key: String) = kvPairDao[key]?.float
|
fun getFloat(key: String) = kvPairDao[key]?.float
|
||||||
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
|
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
|
||||||
|
@ -39,46 +38,33 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
||||||
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
|
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
|
||||||
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
|
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
|
||||||
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
|
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
|
||||||
override fun getStringSet(key: String, defValue: MutableSet<String>?) =
|
override fun getStringSet(key: String, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
|
||||||
getStringSet(key) ?: defValue
|
|
||||||
|
|
||||||
fun putBoolean(key: String, value: Boolean?) =
|
|
||||||
if (value == null) remove(key) else putBoolean(key, value)
|
|
||||||
|
|
||||||
fun putFloat(key: String, value: Float?) =
|
|
||||||
if (value == null) remove(key) else putFloat(key, value)
|
|
||||||
|
|
||||||
fun putInt(key: String, value: Int?) =
|
|
||||||
if (value == null) remove(key) else putLong(key, value.toLong())
|
|
||||||
|
|
||||||
|
fun putBoolean(key: String, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
|
||||||
|
fun putFloat(key: String, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
|
||||||
|
fun putInt(key: String, value: Int?) = if (value == null) remove(key) else putLong(key, value.toLong())
|
||||||
fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||||
override fun putBoolean(key: String, value: Boolean) {
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
kvPairDao.put(KeyValuePair(key).put(value))
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
fireChangeListener(key)
|
fireChangeListener(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putFloat(key: String, value: Float) {
|
override fun putFloat(key: String, value: Float) {
|
||||||
kvPairDao.put(KeyValuePair(key).put(value))
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
fireChangeListener(key)
|
fireChangeListener(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putInt(key: String, value: Int) {
|
override fun putInt(key: String, value: Int) {
|
||||||
kvPairDao.put(KeyValuePair(key).put(value.toLong()))
|
kvPairDao.put(KeyValuePair(key).put(value.toLong()))
|
||||||
fireChangeListener(key)
|
fireChangeListener(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putLong(key: String, value: Long) {
|
override fun putLong(key: String, value: Long) {
|
||||||
kvPairDao.put(KeyValuePair(key).put(value))
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
fireChangeListener(key)
|
fireChangeListener(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
|
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
|
||||||
kvPairDao.put(KeyValuePair(key).put(value))
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
fireChangeListener(key)
|
fireChangeListener(key)
|
||||||
}
|
}
|
||||||
|
override fun putStringSet(key: String, values: MutableSet<String>?) = if (values == null) remove(key) else {
|
||||||
override fun putStringSet(key: String, values: MutableSet<String>?) =
|
|
||||||
if (values == null) remove(key) else {
|
|
||||||
kvPairDao.put(KeyValuePair(key).put(values))
|
kvPairDao.put(KeyValuePair(key).put(values))
|
||||||
fireChangeListener(key)
|
fireChangeListener(key)
|
||||||
}
|
}
|
||||||
|
@ -89,12 +75,7 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
||||||
}
|
}
|
||||||
|
|
||||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||||
private fun fireChangeListener(key: String) =
|
private fun fireChangeListener(key: String) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||||
listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
|
||||||
|
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
|
||||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) =
|
|
||||||
listeners.add(listener)
|
|
||||||
|
|
||||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) =
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
}
|
|
@ -18,10 +18,11 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.utils
|
package org.amnezia.vpn.shadowsocks.core.utils
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
|
import org.json.JSONArray
|
||||||
|
|
||||||
private sealed class ArrayIterator<out T> : Iterator<T> {
|
private sealed class ArrayIterator<out T> : Iterator<T> {
|
||||||
abstract val size: Int
|
abstract val size: Int
|
||||||
|
@ -35,12 +36,16 @@ private class ClipDataIterator(private val data: ClipData) : ArrayIterator<ClipD
|
||||||
override val size get() = data.itemCount
|
override val size get() = data.itemCount
|
||||||
override fun get(index: Int) = data.getItemAt(index)
|
override fun get(index: Int) = data.getItemAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ClipData.asIterable() = Iterable { ClipDataIterator(this) }
|
fun ClipData.asIterable() = Iterable { ClipDataIterator(this) }
|
||||||
|
|
||||||
|
private class JSONArrayIterator(private val arr: JSONArray) : ArrayIterator<Any>() {
|
||||||
|
override val size get() = arr.length()
|
||||||
|
override fun get(index: Int) = arr.get(index)
|
||||||
|
}
|
||||||
|
fun JSONArray.asIterable() = Iterable { JSONArrayIterator(this) }
|
||||||
|
|
||||||
private class SortedListIterator<out T>(private val list: SortedList<T>) : ArrayIterator<T>() {
|
private class SortedListIterator<out T>(private val list: SortedList<T>) : ArrayIterator<T>() {
|
||||||
override val size get() = list.size()
|
override val size get() = list.size()
|
||||||
override fun get(index: Int) = list[index]
|
override fun get(index: Int) = list[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> SortedList<T>.asIterable() = Iterable { SortedListIterator(this) }
|
fun <T> SortedList<T>.asIterable() = Iterable { SortedListIterator(this) }
|
|
@ -0,0 +1,173 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.core.utils
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commandline objects help handling command lines specifying processes to
|
||||||
|
* execute.
|
||||||
|
*
|
||||||
|
* The class can be used to define a command line as nested elements or as a
|
||||||
|
* helper to define a command line by an application.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* `
|
||||||
|
* <someelement><br></br>
|
||||||
|
* <acommandline executable="/executable/to/run"><br></br>
|
||||||
|
* <argument value="argument 1" /><br></br>
|
||||||
|
* <argument line="argument_1 argument_2 argument_3" /><br></br>
|
||||||
|
* <argument value="argument 4" /><br></br>
|
||||||
|
* </acommandline><br></br>
|
||||||
|
* </someelement><br></br>
|
||||||
|
` *
|
||||||
|
*
|
||||||
|
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
|
||||||
|
*
|
||||||
|
* Adds support for escape character '\'.
|
||||||
|
*/
|
||||||
|
object Commandline {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote the parts of the given array in way that makes them
|
||||||
|
* usable as command line arguments.
|
||||||
|
* @param args the list of arguments to quote.
|
||||||
|
* @return empty string for null or no command, else every argument split
|
||||||
|
* by spaces and quoted by quoting rules.
|
||||||
|
*/
|
||||||
|
fun toString(args: Iterable<String>?): String {
|
||||||
|
// empty path return empty string
|
||||||
|
if (args == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// path containing one or more elements
|
||||||
|
val result = StringBuilder()
|
||||||
|
for (arg in args) {
|
||||||
|
if (result.isNotEmpty()) result.append(' ')
|
||||||
|
(0 until arg.length)
|
||||||
|
.map { arg[it] }
|
||||||
|
.forEach {
|
||||||
|
when (it) {
|
||||||
|
' ', '\\', '"', '\'' -> {
|
||||||
|
result.append('\\') // intentionally no break
|
||||||
|
result.append(it)
|
||||||
|
}
|
||||||
|
else -> result.append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote the parts of the given array in way that makes them
|
||||||
|
* usable as command line arguments.
|
||||||
|
* @param args the list of arguments to quote.
|
||||||
|
* @return empty string for null or no command, else every argument split
|
||||||
|
* by spaces and quoted by quoting rules.
|
||||||
|
*/
|
||||||
|
fun toString(args: Array<String>) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crack a command line.
|
||||||
|
* @param toProcess the command line to process.
|
||||||
|
* @return the command line broken into strings.
|
||||||
|
* An empty or null toProcess parameter results in a zero sized array.
|
||||||
|
*/
|
||||||
|
fun translateCommandline(toProcess: String?): Array<String> {
|
||||||
|
if (toProcess == null || toProcess.isEmpty()) {
|
||||||
|
//no command? no string
|
||||||
|
return arrayOf()
|
||||||
|
}
|
||||||
|
// parse with a simple finite state machine
|
||||||
|
|
||||||
|
val normal = 0
|
||||||
|
val inQuote = 1
|
||||||
|
val inDoubleQuote = 2
|
||||||
|
var state = normal
|
||||||
|
val tok = StringTokenizer(toProcess, "\\\"\' ", true)
|
||||||
|
val result = ArrayList<String>()
|
||||||
|
val current = StringBuilder()
|
||||||
|
var lastTokenHasBeenQuoted = false
|
||||||
|
var lastTokenIsSlash = false
|
||||||
|
|
||||||
|
while (tok.hasMoreTokens()) {
|
||||||
|
val nextTok = tok.nextToken()
|
||||||
|
when (state) {
|
||||||
|
inQuote -> if ("\'" == nextTok) {
|
||||||
|
lastTokenHasBeenQuoted = true
|
||||||
|
state = normal
|
||||||
|
} else {
|
||||||
|
current.append(nextTok)
|
||||||
|
}
|
||||||
|
inDoubleQuote -> if ("\"" == nextTok) {
|
||||||
|
if (lastTokenIsSlash) {
|
||||||
|
current.append(nextTok)
|
||||||
|
lastTokenIsSlash = false
|
||||||
|
} else {
|
||||||
|
lastTokenHasBeenQuoted = true
|
||||||
|
state = normal
|
||||||
|
}
|
||||||
|
} else if ("\\" == nextTok) {
|
||||||
|
lastTokenIsSlash = if (lastTokenIsSlash) {
|
||||||
|
current.append(nextTok)
|
||||||
|
false
|
||||||
|
} else
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
if (lastTokenIsSlash) {
|
||||||
|
current.append("\\") // unescaped
|
||||||
|
lastTokenIsSlash = false
|
||||||
|
}
|
||||||
|
current.append(nextTok)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (lastTokenIsSlash) {
|
||||||
|
current.append(nextTok)
|
||||||
|
lastTokenIsSlash = false
|
||||||
|
} else if ("\\" == nextTok)
|
||||||
|
lastTokenIsSlash = true
|
||||||
|
else if ("\'" == nextTok) {
|
||||||
|
state = inQuote
|
||||||
|
} else if ("\"" == nextTok) {
|
||||||
|
state = inDoubleQuote
|
||||||
|
} else if (" " == nextTok) {
|
||||||
|
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
|
||||||
|
result.add(current.toString())
|
||||||
|
current.setLength(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current.append(nextTok)
|
||||||
|
}
|
||||||
|
lastTokenHasBeenQuoted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
|
||||||
|
result.add(current.toString())
|
||||||
|
}
|
||||||
|
if (state == inQuote || state == inDoubleQuote) {
|
||||||
|
throw IllegalArgumentException("unbalanced quotes in $toProcess")
|
||||||
|
}
|
||||||
|
if (lastTokenIsSlash) throw IllegalArgumentException("escape character following nothing in $toProcess")
|
||||||
|
return result.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.utils
|
package org.amnezia.vpn.shadowsocks.core.utils
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
/**
|
/**
|
||||||
|
@ -30,13 +30,27 @@ object Key {
|
||||||
const val id = "profileId"
|
const val id = "profileId"
|
||||||
const val name = "profileName"
|
const val name = "profileName"
|
||||||
|
|
||||||
|
const val individual = "Proxyed"
|
||||||
|
|
||||||
|
const val serviceMode = "serviceMode"
|
||||||
|
const val modeProxy = "proxy"
|
||||||
|
const val modeVpn = "vpn"
|
||||||
|
const val modeTransproxy = "transproxy"
|
||||||
|
const val shareOverLan = "shareOverLan"
|
||||||
const val portProxy = "portProxy"
|
const val portProxy = "portProxy"
|
||||||
const val portLocalDns = "portLocalDns"
|
const val portLocalDns = "portLocalDns"
|
||||||
|
const val portTransproxy = "portTransproxy"
|
||||||
|
|
||||||
|
const val route = "route"
|
||||||
|
|
||||||
|
const val isAutoConnect = "isAutoConnect"
|
||||||
const val directBootAware = "directBootAware"
|
const val directBootAware = "directBootAware"
|
||||||
|
|
||||||
|
const val proxyApps = "isProxyApps"
|
||||||
|
const val bypass = "isBypassApps"
|
||||||
const val udpdns = "isUdpDns"
|
const val udpdns = "isUdpDns"
|
||||||
const val ipv6 = "isIpv6"
|
const val ipv6 = "isIpv6"
|
||||||
|
const val metered = "metered"
|
||||||
|
|
||||||
const val host = "proxy"
|
const val host = "proxy"
|
||||||
const val password = "sitekey"
|
const val password = "sitekey"
|
||||||
|
@ -44,17 +58,26 @@ object Key {
|
||||||
const val remotePort = "remotePortNum"
|
const val remotePort = "remotePortNum"
|
||||||
const val remoteDns = "remoteDns"
|
const val remoteDns = "remoteDns"
|
||||||
|
|
||||||
|
const val plugin = "plugin"
|
||||||
|
const val pluginConfigure = "plugin.configure"
|
||||||
|
const val udpFallback = "udpFallback"
|
||||||
|
|
||||||
const val dirty = "profileDirty"
|
const val dirty = "profileDirty"
|
||||||
|
|
||||||
const val tfo = "tcp_fastopen"
|
const val tfo = "tcp_fastopen"
|
||||||
const val hosts = "hosts"
|
|
||||||
const val assetUpdateTime = "assetUpdateTime"
|
const val assetUpdateTime = "assetUpdateTime"
|
||||||
|
|
||||||
|
// TV specific values
|
||||||
|
const val controlStats = "control.stats"
|
||||||
|
const val controlImport = "control.import"
|
||||||
|
const val controlExport = "control.export"
|
||||||
|
const val about = "about"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Action {
|
object Action {
|
||||||
const val SERVICE = "com.github.shadowsocks.SERVICE"
|
const val SERVICE = "com.kyle.shadowsocks.SERVICE"
|
||||||
const val CLOSE = "com.github.shadowsocks.CLOSE"
|
const val CLOSE = "com.kyle.shadowsocks.CLOSE"
|
||||||
const val RELOAD = "com.github.shadowsocks.RELOAD"
|
const val RELOAD = "com.kyle.shadowsocks.RELOAD"
|
||||||
|
|
||||||
const val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
|
const val EXTRA_PROFILE_ID = "com.kyle.shadowsocks.EXTRA_PROFILE_ID"
|
||||||
}
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.utils
|
package org.amnezia.vpn.shadowsocks.core.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
|
@ -1,16 +1,16 @@
|
||||||
package com.github.shadowsocks.utils
|
package org.amnezia.vpn.shadowsocks.core.utils
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import com.github.shadowsocks.Core
|
import org.amnezia.vpn.shadowsocks.core.Core
|
||||||
import com.github.shadowsocks.Core.app
|
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||||
import com.github.shadowsocks.bg.BaseService
|
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||||
import com.github.shadowsocks.database.Profile
|
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||||
import com.github.shadowsocks.database.ProfileManager
|
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||||
import com.github.shadowsocks.preference.DataStore
|
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.ObjectInputStream
|
import java.io.ObjectInputStream
|
||||||
|
@ -23,9 +23,7 @@ object DirectBoot : BroadcastReceiver() {
|
||||||
|
|
||||||
fun getDeviceProfile(): Pair<Profile, Profile?>? = try {
|
fun getDeviceProfile(): Pair<Profile, Profile?>? = try {
|
||||||
ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> }
|
ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> }
|
||||||
} catch (_: IOException) {
|
} catch (_: IOException) { null }
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clean() {
|
fun clean() {
|
||||||
file.delete()
|
file.delete()
|
||||||
|
@ -38,9 +36,7 @@ object DirectBoot : BroadcastReceiver() {
|
||||||
*/
|
*/
|
||||||
fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) =
|
fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) =
|
||||||
if (profile == null) clean()
|
if (profile == null) clean()
|
||||||
else ObjectOutputStream(file.outputStream()).use {
|
else ObjectOutputStream(file.outputStream()).use { it.writeObject(ProfileManager.expand(profile)) }
|
||||||
it.writeObject(ProfileManager.expand(profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun flushTrafficStats() {
|
fun flushTrafficStats() {
|
||||||
getDeviceProfile()?.also { (profile, fallback) ->
|
getDeviceProfile()?.also { (profile, fallback) ->
|
||||||
|
@ -55,7 +51,6 @@ object DirectBoot : BroadcastReceiver() {
|
||||||
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
|
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
|
||||||
registered = true
|
registered = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
flushTrafficStats()
|
flushTrafficStats()
|
||||||
app.unregisterReceiver(this)
|
app.unregisterReceiver(this)
|
|
@ -18,9 +18,8 @@
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
package com.github.shadowsocks.utils
|
package org.amnezia.vpn.shadowsocks.core.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -36,64 +35,30 @@ import android.system.OsConstants
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) {
|
|
||||||
var result: Exception? = null
|
|
||||||
for (element in this) try {
|
|
||||||
action(element)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (result == null) result = e else result.addSuppressed(e)
|
|
||||||
}
|
|
||||||
if (result != null) {
|
|
||||||
result.printStackTrace()
|
|
||||||
throw result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
|
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
|
||||||
|
|
||||||
private val parseNumericAddress by lazy @SuppressLint("DiscouragedPrivateApi") {
|
private val parseNumericAddress by lazy {
|
||||||
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
||||||
isAccessible = true
|
isAccessible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A slightly more performant variant of parseNumericAddress.
|
* A slightly more performant variant of InetAddress.parseNumericAddress.
|
||||||
*
|
*
|
||||||
* Bug in Android 9.0 and lower: https://issuetracker.google.com/issues/123456213
|
* Bug: https://issuetracker.google.com/issues/123456213
|
||||||
*/
|
*/
|
||||||
fun String?.parseNumericAddress(): InetAddress? =
|
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
|
||||||
Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let {
|
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
|
||||||
if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null,
|
|
||||||
this) as InetAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) =
|
fun HttpURLConnection.disconnectFromMain() {
|
||||||
if (Build.VERSION.SDK_INT >= 24) computeIfAbsent(key) { value() } else this[key]
|
|
||||||
?: value().also { put(key, it) }
|
|
||||||
|
|
||||||
suspend fun <T> HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T {
|
|
||||||
return suspendCancellableCoroutine { cont ->
|
|
||||||
cont.invokeOnCancellation {
|
|
||||||
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
|
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
|
||||||
}
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
cont.resume(block())
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
cont.resumeWithException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
||||||
|
@ -101,17 +66,16 @@ fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
||||||
return if (value < min || value > 65535) default else value
|
return if (value < min || value > 65535) default else value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
|
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
|
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ContentResolver.openBitmap(uri: Uri) =
|
fun ContentResolver.openBitmap(uri: Uri) =
|
||||||
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
|
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
|
||||||
else BitmapFactory.decodeStream(openInputStream(uri))
|
else BitmapFactory.decodeStream(openInputStream(uri))
|
||||||
|
|
||||||
val PackageInfo.signaturesCompat
|
val PackageInfo.signaturesCompat get() =
|
||||||
get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
|
if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on: https://stackoverflow.com/a/26348729/2245107
|
* Based on: https://stackoverflow.com/a/26348729/2245107
|
||||||
|
@ -122,11 +86,9 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
|
||||||
return typedValue.resourceId
|
return typedValue.resourceId
|
||||||
}
|
}
|
||||||
|
|
||||||
val Intent.datas
|
val Intent.datas get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
|
||||||
get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
|
|
||||||
|
|
||||||
fun printLog(t: Throwable) {
|
fun printLog(t: Throwable) {
|
||||||
// Crashlytics.logException(t)
|
|
||||||
t.printStackTrace()
|
t.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.plugin
|
||||||
|
|
||||||
|
import android.content.ContentProvider
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
|
||||||
|
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
|
||||||
|
* class, implement the abstract methods, and add it to your manifest like this:
|
||||||
|
*
|
||||||
|
* <pre class="prettyprint"><manifest>
|
||||||
|
* ...
|
||||||
|
* <application>
|
||||||
|
* ...
|
||||||
|
* <provider android:name="com.kyle.shadowsocks.$PLUGIN_ID.BinaryProvider"
|
||||||
|
* android:authorities="com.kyle.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
|
||||||
|
* <intent-filter>
|
||||||
|
* <category android:name="com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||||
|
* </intent-filter>
|
||||||
|
* </provider>
|
||||||
|
* ...
|
||||||
|
* </application>
|
||||||
|
*</manifest></pre>
|
||||||
|
*/
|
||||||
|
abstract class NativePluginProvider : ContentProvider() {
|
||||||
|
override fun getType(p0: Uri): String = "application/x-elf"
|
||||||
|
|
||||||
|
override fun onCreate(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide all files needed for native plugin.
|
||||||
|
*
|
||||||
|
* @param provider A helper object to use to add files.
|
||||||
|
*/
|
||||||
|
protected abstract fun populateFiles(provider: PathProvider)
|
||||||
|
|
||||||
|
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
|
||||||
|
sortOrder: String?): Cursor {
|
||||||
|
check(selection == null && selectionArgs == null && sortOrder == null)
|
||||||
|
val result = MatrixCursor(projection)
|
||||||
|
populateFiles(PathProvider(uri, result))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
|
||||||
|
*
|
||||||
|
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
|
||||||
|
* default behavior.
|
||||||
|
*
|
||||||
|
* @return Absolute path for executable entry.
|
||||||
|
*/
|
||||||
|
open fun getExecutable(): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
abstract fun openFile(uri: Uri?): ParcelFileDescriptor
|
||||||
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||||
|
check(mode == "r")
|
||||||
|
return openFile(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
|
||||||
|
PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))
|
||||||
|
else -> super.call(method, arg, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods that should not be used
|
||||||
|
override fun insert(p0: Uri, p1: ContentValues?): Uri = throw UnsupportedOperationException()
|
||||||
|
override fun update(p0: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
override fun delete(uri: Uri, p1: String?, p2: Array<out String>?): Int = throw UnsupportedOperationException()
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.plugin
|
||||||
|
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.net.Uri
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to provide relative paths of files to copy.
|
||||||
|
*/
|
||||||
|
class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
|
||||||
|
private val basePath = baseUri.path?.trim('/') ?: ""
|
||||||
|
|
||||||
|
fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
|
||||||
|
val trimmed = path.trim('/')
|
||||||
|
if (trimmed.startsWith(basePath)) cursor.newRow()
|
||||||
|
.add(PluginContract.COLUMN_PATH, trimmed)
|
||||||
|
.add(PluginContract.COLUMN_MODE, mode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
|
||||||
|
var sub = to + file.name
|
||||||
|
if (basePath.startsWith(sub)) if (file.isDirectory) {
|
||||||
|
sub += '/'
|
||||||
|
file.listFiles().forEach { addTo(it, sub, mode) }
|
||||||
|
} else addPath(sub, mode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
|
||||||
|
if (basePath.startsWith(at))
|
||||||
|
if (file.isDirectory) file.listFiles().forEach { addTo(it, at, mode) } else addPath(at, mode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.plugin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
|
||||||
|
*
|
||||||
|
* This class is written in Java to keep Java interoperability.
|
||||||
|
*/
|
||||||
|
object PluginContract {
|
||||||
|
/**
|
||||||
|
* ContentProvider Action: Used for NativePluginProvider.
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||||
|
*/
|
||||||
|
const val ACTION_NATIVE_PLUGIN = "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity Action: Used for ConfigurationActivity.
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||||
|
*/
|
||||||
|
const val ACTION_CONFIGURE = "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||||
|
/**
|
||||||
|
* Activity Action: Used for HelpActivity or HelpCallback.
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_HELP"
|
||||||
|
*/
|
||||||
|
const val ACTION_HELP = "com.kyle.shadowsocks.plugin.ACTION_HELP"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lookup key for a string that provides the plugin entry binary.
|
||||||
|
*
|
||||||
|
* Example: "/data/data/com.kyle.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
|
||||||
|
*/
|
||||||
|
const val EXTRA_ENTRY = "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
|
||||||
|
/**
|
||||||
|
* The lookup key for a string that provides the options as a string.
|
||||||
|
*
|
||||||
|
* Example: "obfs=http;obfs-host=www.baidu.com"
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||||
|
*/
|
||||||
|
const val EXTRA_OPTIONS = "com.kyle.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||||
|
/**
|
||||||
|
* The lookup key for a CharSequence that provides user relevant help message.
|
||||||
|
*
|
||||||
|
* Example: "obfs=<http></http>|tls> Enable obfuscating: HTTP or TLS (Experimental).
|
||||||
|
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||||
|
</host_name> */
|
||||||
|
const val EXTRA_HELP_MESSAGE = "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata key to retrieve plugin id. Required for plugins.
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.id"
|
||||||
|
*/
|
||||||
|
const val METADATA_KEY_ID = "com.kyle.shadowsocks.plugin.id"
|
||||||
|
/**
|
||||||
|
* The metadata key to retrieve default configuration. Default value is empty.
|
||||||
|
*
|
||||||
|
* Constant Value: "com.kyle.shadowsocks.plugin.default_config"
|
||||||
|
*/
|
||||||
|
const val METADATA_KEY_DEFAULT_CONFIG = "com.kyle.shadowsocks.plugin.default_config"
|
||||||
|
|
||||||
|
const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
|
||||||
|
|
||||||
|
/** ConfigurationActivity result: fallback to manual edit mode. */
|
||||||
|
const val RESULT_FALLBACK = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative to the file to be copied. This column is required.
|
||||||
|
*
|
||||||
|
* Example: "kcptun", "doc/help.txt"
|
||||||
|
*
|
||||||
|
* Type: String
|
||||||
|
*/
|
||||||
|
const val COLUMN_PATH = "path"
|
||||||
|
/**
|
||||||
|
* File mode bits. Default value is "644".
|
||||||
|
*
|
||||||
|
* Example: "755"
|
||||||
|
*
|
||||||
|
* Type: String
|
||||||
|
*/
|
||||||
|
const val COLUMN_MODE = "mode"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scheme for general plugin actions.
|
||||||
|
*/
|
||||||
|
const val SCHEME = "plugin"
|
||||||
|
/**
|
||||||
|
* The authority for general plugin actions.
|
||||||
|
*/
|
||||||
|
const val AUTHORITY = "com.kyle.shadowsocks"
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
package org.amnezia.vpn.shadowsocks.plugin
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for processing plugin options.
|
||||||
|
*
|
||||||
|
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
|
||||||
|
*/
|
||||||
|
class PluginOptions : HashMap<String, String?> {
|
||||||
|
var id = ""
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
constructor(initialCapacity: Int) : super(initialCapacity)
|
||||||
|
constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
|
||||||
|
|
||||||
|
private constructor(options: String?, parseId: Boolean) : this() {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
var parseId = parseId
|
||||||
|
if (options.isNullOrEmpty()) return
|
||||||
|
check(options.all { !it.isISOControl() }) { "No control characters allowed." }
|
||||||
|
val tokenizer = StringTokenizer("$options;", "\\=;", true)
|
||||||
|
val current = StringBuilder()
|
||||||
|
var key: String? = null
|
||||||
|
while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
|
||||||
|
"\\" -> current.append(tokenizer.nextToken())
|
||||||
|
"=" -> if (key == null) {
|
||||||
|
key = current.toString()
|
||||||
|
current.setLength(0)
|
||||||
|
} else current.append(nextToken)
|
||||||
|
";" -> {
|
||||||
|
if (key != null) {
|
||||||
|
put(key, current.toString())
|
||||||
|
key = null
|
||||||
|
} else if (current.isNotEmpty())
|
||||||
|
if (parseId) id = current.toString() else put(current.toString(), null)
|
||||||
|
current.setLength(0)
|
||||||
|
parseId = false
|
||||||
|
}
|
||||||
|
else -> current.append(nextToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: String?) : this(options, true)
|
||||||
|
constructor(id: String, options: String?) : this(options, false) {
|
||||||
|
this.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put but if value is null or default, the entry is deleted.
|
||||||
|
*
|
||||||
|
* @return Old value before put.
|
||||||
|
*/
|
||||||
|
fun putWithDefault(key: String, value: String?, default: String? = null) =
|
||||||
|
if (value == null || value == default) remove(key) else put(key, value)
|
||||||
|
|
||||||
|
private fun append(result: StringBuilder, str: String) = (0 until str.length)
|
||||||
|
.map { str[it] }
|
||||||
|
.forEach {
|
||||||
|
when (it) {
|
||||||
|
'\\', '=', ';' -> {
|
||||||
|
result.append('\\') // intentionally no break
|
||||||
|
result.append(it)
|
||||||
|
}
|
||||||
|
else -> result.append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toString(trimId: Boolean): String {
|
||||||
|
val result = StringBuilder()
|
||||||
|
if (!trimId) if (id.isEmpty()) return "" else append(result, id)
|
||||||
|
for ((key, value) in entries) {
|
||||||
|
if (result.isNotEmpty()) result.append(';')
|
||||||
|
append(result, key)
|
||||||
|
if (value != null) {
|
||||||
|
result.append('=')
|
||||||
|
append(result, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = toString(true)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
|
||||||
|
}
|
||||||
|
override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
|
||||||
|
}
|
|
@ -273,6 +273,7 @@ android {
|
||||||
|
|
||||||
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libss-local.so
|
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libss-local.so
|
||||||
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libtun2socks.so
|
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libtun2socks.so
|
||||||
|
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libredsocks.so
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,5 +371,7 @@ ios {
|
||||||
# %{sourceDir}/client/ios/xcode_patcher.rb %{buildDir}/client/AmneziaVPN.xcodeproj 2.0 2.0.0 ios 1
|
# %{sourceDir}/client/ios/xcode_patcher.rb %{buildDir}/client/AmneziaVPN.xcodeproj 2.0 2.0.0 ios 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DISTFILES +=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case DockerContainer::WireGuard: return true;
|
case DockerContainer::WireGuard: return true;
|
||||||
case DockerContainer::OpenVpn: return true;
|
case DockerContainer::OpenVpn: return true;
|
||||||
|
case DockerContainer::ShadowSocks: return true;
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue