mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
Android shadowsocks code added
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
<!-- %%INSERT_FEATURES -->
|
||||
|
||||
<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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@@ -76,17 +77,21 @@
|
||||
<!-- extract android style -->
|
||||
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
|
||||
</activity>
|
||||
|
||||
<service android:name=".VPNService"
|
||||
android:process=":QtOnlyProcess"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<service android:name=".VPNService" android:process=":QtOnlyProcess">
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
|
||||
<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 android:name="org.amnezia.vpn.qt.VPNPermissionHelper"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper">
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
</service>
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</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;
|
||||
+2
-2
@@ -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 {
|
||||
int getState();
|
||||
+18
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,10 @@
|
||||
127.0.0.0/8
|
||||
169.254.0.0/16
|
||||
172.16.0.0/12
|
||||
192.0.0.0/24
|
||||
192.0.0.0/29
|
||||
192.0.2.0/24
|
||||
192.31.196.0/24
|
||||
192.52.193.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
192.175.48.0/24
|
||||
198.18.0.0/15
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+35
-18
@@ -1,6 +1,8 @@
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
buildscript {
|
||||
ext{
|
||||
kotlin_version = "1.5.0"
|
||||
kotlin_version = "1.4.30-M1"
|
||||
// for libwg
|
||||
appcompatVersion = '1.1.0'
|
||||
annotationsVersion = '1.0.1'
|
||||
@@ -19,6 +21,8 @@ buildscript {
|
||||
|
||||
dependencies {
|
||||
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-serialization:$kotlin_version"
|
||||
}
|
||||
@@ -34,24 +38,44 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
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-identity-credential:1.0.0-alpha02"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||
//ss
|
||||
implementation "androidx.preference:preference:1.1.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.3.4"
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||
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.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 "dnsjava:dnsjava:2.1.9"
|
||||
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 {
|
||||
@@ -86,6 +110,7 @@ android {
|
||||
renderscript.srcDirs = ['src']
|
||||
assets.srcDirs = ['assets']
|
||||
jniLibs.srcDirs = ['libs']
|
||||
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +139,10 @@ android {
|
||||
targetSdkVersion = 30
|
||||
versionCode 8 // Change to a higher number
|
||||
versionName "2.0.8" // Change to a higher number
|
||||
|
||||
javaCompileOptions.annotationProcessorOptions.arguments = [
|
||||
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString()
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# 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
|
||||
# build with the same inputs. However, over time, the cache size will
|
||||
@@ -21,3 +22,7 @@ androidBuildToolsVersion=30.0.2
|
||||
androidCompileSdkVersion=30
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
android.enableJetifier=true
|
||||
android.injected.testOnly=false
|
||||
kapt.use.worker.api=false
|
||||
kapt.incremental.apt=false
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,113 +1,169 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Shadowsocks</string>
|
||||
<string name="send_email">Send email</string>
|
||||
<string name="app_name">shadowsocks</string>
|
||||
|
||||
<!-- ssplugin -->
|
||||
<string name="proxy_cat">Server Settings</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="no">No</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="file_manager_missing">File Explorer Missing</string>
|
||||
<string name="browse">Browse…</string>
|
||||
<string name="service_mode_vpn">VPN</string>
|
||||
<string name="speed">%s/s</string>
|
||||
|
||||
|
||||
<string name="quick_toggle">"Switch"</string>
|
||||
<string name="remote_dns">"Remote DNS"</string>
|
||||
<string name="stat_summary">"Upload: \t%3$s\t↑\t%1$s
|
||||
Download: \t%4$s\t↓\t%2$s"</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 -->
|
||||
<string name="profile_name">Profile Name</string>
|
||||
<string name="proxy">Server</string>
|
||||
<string name="remote_port">Remote Port</string>
|
||||
<string name="sitekey">Password</string>
|
||||
<string name="enc_method">Encrypt Method</string>
|
||||
<string name="profile_name">"Profile name"</string>
|
||||
<string name="proxy">"Server"</string>
|
||||
<string name="remote_port">"Remote Port"</string>
|
||||
<string name="sitekey">"Password"</string>
|
||||
<string name="enc_method">"Encryption"</string>
|
||||
|
||||
<!-- feature category -->
|
||||
<string name="ipv6">IPv6 Route</string>
|
||||
<string name="ipv6_summary">Redirect IPv6 traffic to remote</string>
|
||||
<string name="on">On</string>
|
||||
<string name="off">Off</string>
|
||||
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
|
||||
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s < 3.7.1</string>
|
||||
<string name="tcp_fastopen_failure">Toggle failed</string>
|
||||
<string name="udp_dns">Send DNS over UDP</string>
|
||||
<string name="udp_dns_summary">Requires UDP forwarding on server side</string>
|
||||
<string name="ipv6">"IPv6 routing"</string>
|
||||
<string name="ipv6_summary">"Forward IPv6 traffic to remote server"</string>
|
||||
<string name="route_list">"Routing"</string>
|
||||
<string name="route_entry_gfwlist">"GFW List"</string>
|
||||
<string name="proxied_apps">"Proxied VPN"</string>
|
||||
<string name="proxied_apps_summary">"Allow some apps to bypass VPN"</string>
|
||||
<string name="on">"On"</string>
|
||||
<string name="bypass_apps">"Bypass"</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 -->
|
||||
<string name="service_vpn">VPN Service</string>
|
||||
<string name="forward_success">Shadowsocks started.</string>
|
||||
<string name="invalid_server">Invalid server name</string>
|
||||
<string name="service_failed">Failed to connect the remote server</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="stopping">Shutting down…</string>
|
||||
<string name="vpn_error">%s</string>
|
||||
<string name="vpn_permission_denied">Permission denied to create a VPN service</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>
|
||||
<string name="forward_success">"Background service has started running. "</string>
|
||||
<string name="invalid_server">"Invalid server name"</string>
|
||||
<string name="service_failed">"Unable to connect to remote server"</string>
|
||||
<string name="stop">"Stop"</string>
|
||||
<string name="stopping">"stopping…"</string>
|
||||
<string name="vpn_error">"Background service failed to start: %s"</string>
|
||||
<string name="reboot_required">"VPN service failed to start. You may need to restart your device."</string>
|
||||
<string name="profile_invalid_input">"No valid configuration file found."</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="profile_empty">Please select a profile</string>
|
||||
<string name="proxy_empty">Proxy/Password should not be empty</string>
|
||||
<string name="connect">Connect</string>
|
||||
<string name="profile_empty">"Please select a profile"</string>
|
||||
<string name="proxy_empty">"The proxy server address and password cannot be empty"</string>
|
||||
<string name="connect">"Connect"</string>
|
||||
|
||||
<!-- menu category -->
|
||||
<string name="profiles">Profiles</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="about">About</string>
|
||||
<string name="about_title">Shadowsocks %s</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="add_profile">Add Profile</string>
|
||||
<string name="action_apply_all">Apply Settings to All Profiles</string>
|
||||
<string name="action_export_more">Export…</string>
|
||||
<string name="action_export_file">Export to file…</string>
|
||||
<string name="action_export">Export to Clipboard</string>
|
||||
<string name="action_import">Import from Clipboard</string>
|
||||
<string name="action_import_file">Import from file…</string>
|
||||
<string name="action_replace_file">Replace from file…</string>
|
||||
<string name="action_export_msg">Successfully export!</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>
|
||||
<string name="profiles">"Profiles"</string>
|
||||
<string name="settings">"Settings"</string>
|
||||
<string name="faq">"FAQ"</string>
|
||||
<string name="about">"About"</string>
|
||||
<string name="about_title">"Shadowsocks %s"</string>
|
||||
<string name="edit">"Edit"</string>
|
||||
<string name="share">"Share"</string>
|
||||
<string name="add_profile">"Add Profile"</string>
|
||||
<string name="action_apply_all">"Apply settings to all profiles"</string>
|
||||
<string name="action_export">"Export to clipboard"</string>
|
||||
<string name="action_import">"Import from clipboard"</string>
|
||||
<string name="action_export_msg">"Export to clipboard succeeded"</string>
|
||||
<string name="action_export_err">"Export to clipboard failed"</string>
|
||||
<string name="action_import_msg">"Import successful"</string>
|
||||
<string name="action_import_err">"Import failed"</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_config">Profile config</string>
|
||||
<string name="delete">Remove</string>
|
||||
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
|
||||
<string name="share_qr_nfc">QR code</string>
|
||||
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
|
||||
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
|
||||
<string name="add_profile_methods_manual_settings">Manual Settings</string>
|
||||
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="profile_config">"Profile Config"</string>
|
||||
<string name="delete">"Delete"</string>
|
||||
<string name="delete_confirm_prompt">"Are you sure you want to delete this profile?"</string>
|
||||
<string name="share_qr_nfc">"QR code / NFC"</string>
|
||||
<string name="add_profile_dialog">"Add this profile for Shadowsock?"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"Scan QR code"</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"%d items deleted"</item>
|
||||
</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 -->
|
||||
<string name="connecting">Connecting…</string>
|
||||
<string name="vpn_connected">Connected, tap to check connection</string>
|
||||
<string name="not_connected">Not connected</string>
|
||||
<string name="sent">"Send: "</string>
|
||||
<string name="received">"Received:"</string>
|
||||
|
||||
<string name="sent">Sent</string>
|
||||
<string name="received">Received</string>
|
||||
<!-- status -->
|
||||
<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 -->
|
||||
<string name="add_first_profile">There is no profile currently, would you like to add it now?</string>
|
||||
<string name="port_proxy">SOCKS5 proxy port</string>
|
||||
<string name="port_local_dns">Local DNS port</string>
|
||||
<string name="advanced">"Advanced options"</string>
|
||||
|
||||
<string name="quick_toggle">Toggle</string>
|
||||
<string name="remote_dns">Remote DNS</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="connection_test_pending">Check Connectivity</string>
|
||||
<string name="connection_test_testing">Testing…</string>
|
||||
<string name="connection_test_available">Success: HTTPS handshake took %dms</string>
|
||||
<string name="connection_test_error">Fail to detect internet connection: %s</string>
|
||||
<string name="connection_test_fail">Internet Unavailable</string>
|
||||
<string name="connection_test_error_status_code">Error code: #%d</string>
|
||||
|
||||
<string name="speed" translatable="false">%s/s</string>
|
||||
<string name="traffic" translatable="false">%1$s↑\t%2$s↓</string>
|
||||
|
||||
<plurals name="removed">
|
||||
<item quantity="one">Removed</item>
|
||||
<item quantity="other">%d items removed</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
<!-- misc -->
|
||||
<string name="service_mode">"Service mode"</string>
|
||||
<string name="service_mode_proxy">"Proxy only"</string>
|
||||
<string name="service_mode_transproxy">"Transparent proxy"</string>
|
||||
<string name="port_proxy">"SOCKS5 proxy port"</string>
|
||||
<string name="port_local_dns">"local DNS port"</string>
|
||||
<string name="port_transproxy">"Transparent proxy port"</string>
|
||||
<string name="service_proxy">"Proxy mode"</string>
|
||||
<string name="service_transproxy">"Transparent proxy mode"</string>
|
||||
<string name="vpn_permission_denied">"Insufficient permission to create VPN service"</string>
|
||||
<string name="auto_connect_summary_v24">"Allow Shadowsocks to start with the system, an always-on VPN is recommended"</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>
|
||||
<string name="acl_rule_online_config">"Online Rules File URL"</string>
|
||||
<string name="action_import_file">"Import from file…"</string>
|
||||
<string name="night_mode">"Night Mode"</string>
|
||||
<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>
|
||||
@@ -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()}")
|
||||
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
|
||||
mAlreadyInitialised = true
|
||||
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
@@ -82,10 +81,7 @@ class VPNService : android.net.VpnService() {
|
||||
val lastConfString = prefs.getString("lastConf", "")
|
||||
if (lastConfString.isNullOrEmpty()) {
|
||||
// We have nothing to connect to -> Exit
|
||||
Log.e(
|
||||
tag,
|
||||
"VPN service was triggered without defining a Server or having a tunnel"
|
||||
)
|
||||
Log.e(tag,"VPN service was triggered without defining a Server or having a tunnel")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
this.mConfig = JSONObject(lastConfString)
|
||||
@@ -156,10 +152,13 @@ class VPNService : android.net.VpnService() {
|
||||
}
|
||||
Log.i(tag, "Permission okay")
|
||||
mConfig = json!!
|
||||
Log.i(tag, "Config: " + mConfig)
|
||||
mProtocol = mConfig!!.getString("protocol")
|
||||
Log.i(tag, "Protocol: " + mProtocol)
|
||||
when (mProtocol) {
|
||||
"openvpn" -> startOpenVpn()
|
||||
"wireguard" -> startWireGuard()
|
||||
"shadowsocks" -> startShadowsocks()
|
||||
else -> {
|
||||
Log.e(tag, "No protocol")
|
||||
return 0
|
||||
@@ -365,6 +364,19 @@ class VPNService : android.net.VpnService() {
|
||||
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() {
|
||||
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
|
||||
Thread({
|
||||
|
||||
@@ -54,7 +54,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
||||
val json = buffer?.let { String(it) }
|
||||
val config = JSONObject(json)
|
||||
Log.v(tag, "Stored new Tunnel config in Service")
|
||||
|
||||
Log.i(tag, "Config: $config")
|
||||
if (!mService.checkPermissions()) {
|
||||
mResumeConfig = config
|
||||
// The Permission prompt was already
|
||||
|
||||
@@ -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;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.amnezia.vpn.BuildConfig;
|
||||
import android.content.res.Configuration;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.amnezia.vpn.shadowsocks.core.Core;
|
||||
import org.amnezia.vpn.shadowsocks.core.VpnManager;
|
||||
|
||||
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
|
||||
private static VPNApplication instance;
|
||||
|
||||
private static VPNApplication instance;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
VPNApplication.instance = this;
|
||||
}
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
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()
|
||||
}
|
||||
}
|
||||
+51
-56
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
@@ -31,7 +31,6 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.UserManager
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -39,17 +38,17 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import com.github.shadowsocks.aidl.ShadowsocksConnection
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.net.TcpFastOpen
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.*
|
||||
|
||||
import org.amnezia.vpn.R
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
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_VALUE_ON
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.R
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.reflect.KClass
|
||||
@@ -59,22 +58,22 @@ object Core {
|
||||
|
||||
lateinit var app: Application
|
||||
lateinit var configureIntent: (Context) -> PendingIntent
|
||||
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
|
||||
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
|
||||
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
|
||||
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
|
||||
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?>?
|
||||
get() {
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile()?.apply { return this }
|
||||
return ProfileManager.expand(ProfileManager.getProfile(DataStore.profileId)
|
||||
?: return null)
|
||||
?: return null)
|
||||
}
|
||||
|
||||
fun switchProfile(id: Long): Profile {
|
||||
@@ -84,37 +83,34 @@ object Core {
|
||||
}
|
||||
|
||||
fun init(app: Application, configureClass: KClass<out Any>) {
|
||||
this.app = app
|
||||
this.configureIntent = {
|
||||
PendingIntent.getActivity(it,
|
||||
0,
|
||||
Intent(it,
|
||||
configureClass.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
0)
|
||||
Core.app = app
|
||||
configureIntent = {
|
||||
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||
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
|
||||
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
||||
// Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
|
||||
// FirebaseApp.initializeApp(deviceStorage)
|
||||
WorkManager.initialize(deviceStorage, Configuration.Builder().apply {
|
||||
setExecutor { GlobalScope.launch { it.run() } }
|
||||
setTaskExecutor { GlobalScope.launch { it.run() } }
|
||||
}.build())
|
||||
WorkManager.initialize(deviceStorage, Configuration.Builder().build())
|
||||
|
||||
// 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.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
|
||||
val assetManager = app.assets
|
||||
try {
|
||||
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
|
||||
File(deviceStorage.noBackupFilesDir, file).outputStream()
|
||||
.use { output -> input.copyTo(output) }
|
||||
File(ContextCompat.getNoBackupFilesDir(deviceStorage), file).outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
printLog(e)
|
||||
@@ -127,39 +123,38 @@ object Core {
|
||||
fun updateNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
val nm = app.getSystemService<NotificationManager>()!!
|
||||
nm.createNotificationChannels(listOf(NotificationChannel("service-vpn",
|
||||
app.getText(R.string.service_vpn),
|
||||
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
|
||||
else NotificationManager.IMPORTANCE_LOW) // #1355
|
||||
))
|
||||
nm.createNotificationChannels(listOf(
|
||||
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fun getPackageInfo(packageName: String) = app.packageManager.getPackageInfo(packageName,
|
||||
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
||||
|
||||
fun startService() =
|
||||
ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
||||
|
||||
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
|
||||
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
|
||||
|
||||
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
||||
object : BroadcastReceiver() {
|
||||
init {
|
||||
app.registerReceiver(this, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
|
||||
callback()
|
||||
if (onetime) app.unregisterReceiver(this)
|
||||
}
|
||||
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() {
|
||||
init {
|
||||
app.registerReceiver(this, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
|
||||
callback()
|
||||
if (onetime) app.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+38
-45
@@ -18,44 +18,50 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.aidl
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import com.github.shadowsocks.LocalVpnService
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
|
||||
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.
|
||||
*/
|
||||
class ShadowsocksConnection(
|
||||
private val handler: Handler = Handler(),
|
||||
private var listenForDeath: Boolean = false
|
||||
) : ServiceConnection,
|
||||
IBinder.DeathRecipient {
|
||||
class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
private var listenForDeath: Boolean = false) :
|
||||
ServiceConnection, IBinder.DeathRecipient {
|
||||
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 {
|
||||
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
||||
fun trafficUpdated(profileId: Long, stats: TrafficStats) {}
|
||||
fun trafficPersisted(profileId: Long) {}
|
||||
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
|
||||
fun trafficPersisted(profileId: Long) { }
|
||||
|
||||
fun onServiceConnected(service: IShadowsocksService)
|
||||
|
||||
/**
|
||||
* Different from Android framework, this method will be called even when you call `detachService`.
|
||||
*/
|
||||
fun onServiceDisconnected() {}
|
||||
|
||||
fun onBinderDied() {}
|
||||
fun onServiceDisconnected() { }
|
||||
fun onBinderDied() { }
|
||||
}
|
||||
|
||||
private var connectionActive = false
|
||||
@@ -64,16 +70,14 @@ class ShadowsocksConnection(
|
||||
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
val callback = callback ?: return
|
||||
handler.post {
|
||||
callback.stateChanged(BaseService.State.values()[state], profileName, msg)
|
||||
}
|
||||
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
|
||||
}
|
||||
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
val callback = callback ?: return
|
||||
handler.post { callback.trafficUpdated(profileId, stats) }
|
||||
handler.post {
|
||||
callback.trafficUpdated(profileId, stats)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trafficPersisted(profileId: Long) {
|
||||
val callback = callback ?: return
|
||||
handler.post { callback.trafficPersisted(profileId) }
|
||||
@@ -83,30 +87,25 @@ class ShadowsocksConnection(
|
||||
|
||||
var bandwidthTimeout = 0L
|
||||
set(value) {
|
||||
try {
|
||||
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)
|
||||
else service?.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: RemoteException) {
|
||||
}
|
||||
val service = service
|
||||
if (bandwidthTimeout != value && service != null)
|
||||
if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
|
||||
service.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: DeadObjectException) { }
|
||||
field = value
|
||||
}
|
||||
var service: IShadowsocksService? = null
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||
this.binder = binder
|
||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||
this.service = service
|
||||
try {
|
||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||
check(!callbackRegistered)
|
||||
if (!callbackRegistered) try {
|
||||
service.registerCallback(serviceCallback)
|
||||
callbackRegistered = true
|
||||
if (bandwidthTimeout > 0) service.startListeningForBandwidth(
|
||||
serviceCallback,
|
||||
bandwidthTimeout
|
||||
)
|
||||
} catch (_: RemoteException) {
|
||||
}
|
||||
if (bandwidthTimeout > 0) service.startListeningForBandwidth(serviceCallback, bandwidthTimeout)
|
||||
} catch (_: RemoteException) { }
|
||||
callback!!.onServiceConnected(service)
|
||||
}
|
||||
|
||||
@@ -119,7 +118,6 @@ class ShadowsocksConnection(
|
||||
|
||||
override fun binderDied() {
|
||||
service = null
|
||||
callbackRegistered = false
|
||||
callback?.also { handler.post(it::onBinderDied) }
|
||||
}
|
||||
|
||||
@@ -127,8 +125,7 @@ class ShadowsocksConnection(
|
||||
val service = service
|
||||
if (service != null && callbackRegistered) try {
|
||||
service.unregisterCallback(serviceCallback)
|
||||
} catch (_: RemoteException) {
|
||||
}
|
||||
} catch (_: RemoteException) { }
|
||||
callbackRegistered = false
|
||||
}
|
||||
|
||||
@@ -145,15 +142,11 @@ class ShadowsocksConnection(
|
||||
unregisterCallback()
|
||||
if (connectionActive) try {
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} // ignore
|
||||
} catch (_: IllegalArgumentException) { } // ignore
|
||||
connectionActive = false
|
||||
if (listenForDeath) binder?.unlinkToDeath(this, 0)
|
||||
binder = null
|
||||
try {
|
||||
service?.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: RemoteException) {
|
||||
}
|
||||
service?.stopListeningForBandwidth(serviceCallback)
|
||||
service = null
|
||||
callback = null
|
||||
}
|
||||
+12
-15
@@ -18,34 +18,31 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.aidl
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
data class TrafficStats(
|
||||
// Bytes per second
|
||||
var txRate: Long = 0L, var rxRate: Long = 0L,
|
||||
// Bytes per second
|
||||
var txRate: Long = 0L,
|
||||
var rxRate: Long = 0L,
|
||||
|
||||
// Bytes for the current session
|
||||
var txTotal: Long = 0L, var rxTotal: Long = 0L) : Parcelable {
|
||||
operator fun plus(other: TrafficStats) = TrafficStats(txRate + other.txRate,
|
||||
rxRate + other.rxRate,
|
||||
txTotal + other.txTotal,
|
||||
rxTotal + other.rxTotal)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readLong(),
|
||||
parcel.readLong(),
|
||||
parcel.readLong(),
|
||||
parcel.readLong())
|
||||
// Bytes for the current session
|
||||
var txTotal: Long = 0L,
|
||||
var rxTotal: Long = 0L
|
||||
) : Parcelable {
|
||||
operator fun plus(other: TrafficStats) = TrafficStats(
|
||||
txRate + other.txRate, rxRate + other.rxRate,
|
||||
txTotal + other.txTotal, rxTotal + other.rxTotal)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readLong(), parcel.readLong(), parcel.readLong())
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeLong(txRate)
|
||||
parcel.writeLong(rxRate)
|
||||
parcel.writeLong(txTotal)
|
||||
parcel.writeLong(rxTotal)
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TrafficStats> {
|
||||
+95
-88
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
@@ -26,16 +26,21 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import androidx.core.content.getSystemService
|
||||
import com.github.shadowsocks.Core
|
||||
import com.github.shadowsocks.Core.app
|
||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.aidl.TrafficStats
|
||||
import com.github.shadowsocks.net.HostsFile
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.*
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
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 java.io.File
|
||||
import java.net.BindException
|
||||
import java.net.InetAddress
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
@@ -50,26 +55,24 @@ object BaseService {
|
||||
* Idle state is only used by UI and will never be returned by BaseService.
|
||||
*/
|
||||
Idle,
|
||||
Connecting(true), Connected(true), Stopping, Stopped,
|
||||
Connecting(true),
|
||||
Connected(true),
|
||||
Stopping,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
const val CONFIG_FILE = "shadowsocks.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) {
|
||||
var state = State.Stopped
|
||||
var processes: GuardedProcessPool? = null
|
||||
var proxy: ProxyInstance? = null
|
||||
// no udpFallback. xinlake
|
||||
var udpFallback: ProxyInstance? = null
|
||||
|
||||
var notification: ServiceNotification? = null
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SHUTDOWN -> service.persistStats()
|
||||
Action.RELOAD -> service.forceLoad()
|
||||
else -> service.stopRunner()
|
||||
}
|
||||
@@ -86,16 +89,15 @@ object BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), CoroutineScope, AutoCloseable {
|
||||
private val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
|
||||
val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
|
||||
super.onCallbackDied(callback, cookie)
|
||||
stopListeningForBandwidth(callback ?: return)
|
||||
}
|
||||
}
|
||||
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
||||
private var looper: Job? = null
|
||||
private val handler = Handler()
|
||||
|
||||
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
|
||||
@@ -105,68 +107,67 @@ object BaseService {
|
||||
}
|
||||
|
||||
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
repeat(count) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: RemoteException) {
|
||||
} catch (e: Exception) {
|
||||
printLog(e)
|
||||
}
|
||||
repeat(callbacks.beginBroadcast()) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: DeadObjectException) {
|
||||
} catch (e: Exception) {
|
||||
printLog(e)
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
|
||||
private suspend fun loop() {
|
||||
while (true) {
|
||||
// delay(bandwidthListeners.values.min() ?: return)
|
||||
delay(5000)
|
||||
val proxies = listOfNotNull(data?.proxy)
|
||||
val stats = proxies.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
private fun registerTimeout() {
|
||||
handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: return)
|
||||
}
|
||||
private fun onTimeout() {
|
||||
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||
val stats = proxies
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) {
|
||||
stats.forEach { (id, stats) -> item.trafficUpdated(id, stats) }
|
||||
item.trafficUpdated(0, sum)
|
||||
}
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) {
|
||||
stats.forEach { (id, stats) -> item.trafficUpdated(id, stats) }
|
||||
item.trafficUpdated(0, sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
registerTimeout()
|
||||
}
|
||||
|
||||
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
|
||||
launch {
|
||||
if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(cb.asBinder(), timeout) == null)) {
|
||||
check(looper == null)
|
||||
looper = launch { loop() }
|
||||
}
|
||||
if (data?.state != State.Connected) return@launch
|
||||
val wasEmpty = bandwidthListeners.isEmpty()
|
||||
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
|
||||
if (wasEmpty) registerTimeout()
|
||||
if (data?.state != State.Connected) return
|
||||
var sum = TrafficStats()
|
||||
val data = data
|
||||
val proxy = data?.proxy ?: return@launch
|
||||
val proxy = data?.proxy ?: return
|
||||
proxy.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
|
||||
sum += 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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||
launch {
|
||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||
looper!!.cancel()
|
||||
looper = null
|
||||
}
|
||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +189,7 @@ object BaseService {
|
||||
|
||||
override fun close() {
|
||||
callbacks.kill()
|
||||
cancel()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
data = null
|
||||
}
|
||||
}
|
||||
@@ -198,13 +199,13 @@ object BaseService {
|
||||
val tag: String
|
||||
fun createNotification(profileName: String): ServiceNotification
|
||||
|
||||
fun onBind(intent: Intent): IBinder? =
|
||||
if (intent.action == Action.SERVICE) data.binder else null
|
||||
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
|
||||
|
||||
fun forceLoad() {
|
||||
val (profile, fallback) = Core.currentProfile
|
||||
?: 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())) {
|
||||
?: 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())) {
|
||||
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
||||
return
|
||||
}
|
||||
@@ -212,21 +213,25 @@ object BaseService {
|
||||
when {
|
||||
s == State.Stopped -> startRunner()
|
||||
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
|
||||
|
||||
suspend fun startProcesses(hosts: HostsFile) {
|
||||
val configRoot =
|
||||
(if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
|
||||
|
||||
suspend fun startProcesses() {
|
||||
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
|
||||
val udpFallback = data.udpFallback
|
||||
data.proxy!!.start(this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
"-u")
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
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() {
|
||||
@@ -247,7 +252,6 @@ object BaseService {
|
||||
// channge the state
|
||||
data.changeState(State.Stopping)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
// Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
|
||||
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
|
||||
this@Interface as Service
|
||||
// we use a coroutineScope here to allow clean-up in parallel
|
||||
@@ -263,11 +267,12 @@ object BaseService {
|
||||
data.notification?.destroy()
|
||||
data.notification = null
|
||||
|
||||
val ids = listOfNotNull(data.proxy).map {
|
||||
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
|
||||
it.shutdown(this)
|
||||
it.profile.id
|
||||
}
|
||||
data.proxy = null
|
||||
data.udpFallback = null
|
||||
data.binder.trafficPersisted(ids)
|
||||
}
|
||||
|
||||
@@ -275,17 +280,12 @@ object BaseService {
|
||||
data.changeState(State.Stopped, msg)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
if (restart) startRunner() else {
|
||||
stopSelf()
|
||||
}
|
||||
if (restart) startRunner() else stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
fun persistStats() =
|
||||
listOfNotNull(data.proxy).forEach { it.trafficMonitor?.persistStats(it.profile.id) }
|
||||
|
||||
suspend fun preInit() {}
|
||||
suspend fun resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host)
|
||||
suspend fun preInit() { }
|
||||
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
|
||||
suspend fun openConnection(url: URL) = url.openConnection()
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -299,10 +299,11 @@ object BaseService {
|
||||
stopRunner(false, getString(R.string.profile_empty))
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
val (profile, _) = profilePair
|
||||
val (profile, fallback) = profilePair
|
||||
profile.name = profile.formattedName // save name for later queries
|
||||
val proxy = ProxyInstance(profile)
|
||||
data.proxy = proxy
|
||||
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
|
||||
if (!data.closeReceiverRegistered) {
|
||||
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||
@@ -314,29 +315,35 @@ object BaseService {
|
||||
}
|
||||
|
||||
data.notification = createNotification(profile.formattedName)
|
||||
// Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
|
||||
|
||||
data.changeState(State.Connecting)
|
||||
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
Executable.killAll() // clean up old processes
|
||||
preInit()
|
||||
val hosts = HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")
|
||||
proxy.init(this@Interface, hosts)
|
||||
proxy.init(this@Interface)
|
||||
data.udpFallback?.init(this@Interface)
|
||||
|
||||
data.processes = GuardedProcessPool {
|
||||
printLog(it)
|
||||
stopRunner(false, it.readableMessage)
|
||||
}
|
||||
startProcesses(hosts)
|
||||
// proxy.scheduleUpdate() // XinLake. Bypass-LAN only
|
||||
startProcesses()
|
||||
|
||||
proxy.scheduleUpdate()
|
||||
data.udpFallback?.scheduleUpdate()
|
||||
|
||||
data.changeState(State.Connected)
|
||||
} catch (_: CancellationException) {
|
||||
// if the job was cancelled, it is canceller's responsibility to call stopRunner
|
||||
} catch (_: UnknownHostException) {
|
||||
stopRunner(false, getString(R.string.invalid_server))
|
||||
} 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}")
|
||||
} finally {
|
||||
data.connectingJob = null
|
||||
+5
-7
@@ -18,25 +18,25 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.text.TextUtils
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object Executable {
|
||||
// libredsocks.so is not required. xinlake
|
||||
const val REDSOCKS = "libredsocks.so"
|
||||
const val SS_LOCAL = "libss-local.so"
|
||||
const val TUN2SOCKS = "libtun2socks.so"
|
||||
|
||||
private val EXECUTABLES = setOf(SS_LOCAL, TUN2SOCKS)
|
||||
private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
|
||||
?: return) {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
val exe = File(try {
|
||||
File(process, "cmdline").inputStream().bufferedReader().readText()
|
||||
} catch (_: IOException) {
|
||||
@@ -47,8 +47,6 @@ object Executable {
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.ESRCH) {
|
||||
e.printStackTrace()
|
||||
// Crashlytics.log(Log.WARN, "kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed")
|
||||
// Crashlytics.logException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-18
@@ -18,15 +18,17 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import androidx.annotation.MainThread
|
||||
import com.github.shadowsocks.Core
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.io.File
|
||||
@@ -60,33 +62,22 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||
val exitChannel = Channel<Int>()
|
||||
try {
|
||||
while (true) {
|
||||
thread(name = "stderr-$cmdName") {
|
||||
streamLogger(process.errorStream) {
|
||||
// Crashlytics.log(Log.ERROR, cmdName, it)
|
||||
}
|
||||
}
|
||||
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
|
||||
thread(name = "stdout-$cmdName") {
|
||||
streamLogger(process.inputStream) {
|
||||
// Crashlytics.log(Log.VERBOSE, cmdName, it)
|
||||
}
|
||||
streamLogger(process.inputStream) { Log.i(cmdName, it) }
|
||||
// this thread also acts as a daemon thread for waitFor
|
||||
runBlocking { exitChannel.send(process.waitFor()) }
|
||||
}
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
val exitCode = exitChannel.receive()
|
||||
running = false
|
||||
when {
|
||||
SystemClock.elapsedRealtime() - startTime < 1000 -> 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"))
|
||||
if (SystemClock.elapsedRealtime() - startTime < 1000) {
|
||||
throw IOException("$cmdName exits too fast (exit code: $exitCode)")
|
||||
}
|
||||
// Crashlytics.log(Log.DEBUG, TAG, "restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
|
||||
start()
|
||||
running = true
|
||||
onRestartCallback?.invoke()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
|
||||
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
|
||||
} finally {
|
||||
if (running) withContext(NonCancellable) {
|
||||
@@ -114,7 +105,6 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||
|
||||
@MainThread
|
||||
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
|
||||
// Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
|
||||
Guard(cmd).apply {
|
||||
start() // if start fails, IOException will be thrown directly
|
||||
launch { looper(onRestartCallback) }
|
||||
+26
-18
@@ -18,39 +18,47 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import com.github.shadowsocks.net.HostsFile
|
||||
import com.github.shadowsocks.net.LocalDnsServer
|
||||
import com.github.shadowsocks.net.Socks5Endpoint
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
|
||||
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 java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.*
|
||||
import org.amnezia.vpn.R
|
||||
|
||||
object LocalDnsService {
|
||||
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>()
|
||||
|
||||
interface Interface : BaseService.Interface {
|
||||
override suspend fun startProcesses(hosts: HostsFile) {
|
||||
super.startProcesses(hosts)
|
||||
override suspend fun startProcesses() {
|
||||
super.startProcesses()
|
||||
val profile = data.proxy!!.profile
|
||||
val dns = try {
|
||||
URI("dns://${profile.remoteDns}")
|
||||
} catch (e: URISyntaxException) {
|
||||
throw BaseService.ExpectedExceptionWrapper(e)
|
||||
}
|
||||
val dns = URI("dns://${profile.remoteDns}")
|
||||
LocalDnsServer(this::resolver,
|
||||
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||
DataStore.proxyAddress,
|
||||
hosts).apply {
|
||||
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||
DataStore.proxyAddress).apply {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+19
-23
@@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* 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 *
|
||||
@@ -18,29 +18,25 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.preference
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.InputFilter
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import androidx.preference.EditTextPreference
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
|
||||
object EditTextPreferenceModifiers {
|
||||
object Monospace : EditTextPreference.OnBindEditTextListener {
|
||||
override fun onBindEditText(editText: EditText) {
|
||||
editText.typeface = Typeface.MONOSPACE
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shadowsocks service at its minimum.
|
||||
*/
|
||||
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 {
|
||||
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))
|
||||
|
||||
override fun onBindEditText(editText: EditText) {
|
||||
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
|
||||
editText.filters = portLengthFilter
|
||||
editText.setSingleLine()
|
||||
editText.setSelection(editText.text.length)
|
||||
}
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
data.binder.close()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+3
-29
@@ -18,15 +18,12 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.os.SystemClock
|
||||
import com.github.shadowsocks.aidl.TrafficStats
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.net.LocalSocketListener
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
@@ -55,7 +52,6 @@ class TrafficMonitor(statFile: File) {
|
||||
var out = TrafficStats()
|
||||
private var timestampLast = 0L
|
||||
private var dirty = false
|
||||
private var persisted: TrafficStats? = null
|
||||
|
||||
fun requestUpdate(): Pair<TrafficStats, Boolean> {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
@@ -83,26 +79,4 @@ class TrafficMonitor(statFile: File) {
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
+77
-55
@@ -18,10 +18,11 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.net.Network
|
||||
@@ -29,14 +30,16 @@ import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import com.github.shadowsocks.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.R
|
||||
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 org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
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.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -45,7 +48,6 @@ import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
|
||||
import java.util.*
|
||||
import android.net.VpnService as BaseVpnService
|
||||
|
||||
@@ -68,46 +70,47 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
}
|
||||
|
||||
private inner class ProtectWorker : ConcurrentLocalSocketListener("ShadowsocksVpnThread",
|
||||
File(Core.deviceStorage.noBackupFilesDir, "protect_path")) {
|
||||
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)
|
||||
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 {
|
||||
inner class NullConnectionException : NullPointerException() {
|
||||
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")
|
||||
ServiceNotification(this, profileName, "service-vpn")
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
private var active = false
|
||||
// metered = false. xinlake
|
||||
private var metered = false
|
||||
private var underlyingNetwork: Network? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (active) setUnderlyingNetworks(underlyingNetworks)
|
||||
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
|
||||
}
|
||||
private val underlyingNetworks
|
||||
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered
|
||||
underlyingNetwork?.let { arrayOf(it) }
|
||||
get() =
|
||||
// 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) {
|
||||
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 {
|
||||
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)
|
||||
if (DataStore.serviceMode == Key.modeVpn) {
|
||||
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 resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
|
||||
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
|
||||
|
||||
override suspend fun startProcesses(hosts: HostsFile) {
|
||||
override suspend fun startProcesses() {
|
||||
worker = ProtectWorker().apply { start() }
|
||||
super.startProcesses(hosts)
|
||||
super.startProcesses()
|
||||
sendFd(startVpn())
|
||||
}
|
||||
|
||||
@@ -157,39 +157,60 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
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)
|
||||
.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)
|
||||
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)
|
||||
}
|
||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
||||
|
||||
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 {
|
||||
val subnet = Subnet.fromString(it)!!
|
||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||
}
|
||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
||||
}
|
||||
}
|
||||
|
||||
metered = profile.metered
|
||||
active = true // possible race condition here?
|
||||
if (Build.VERSION.SDK_INT >= 22) {
|
||||
builder.setUnderlyingNetworks(underlyingNetworks)
|
||||
}
|
||||
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")
|
||||
"--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
|
||||
@@ -199,6 +220,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
try {
|
||||
sendFd(conn.fileDescriptor)
|
||||
} catch (e: ErrnoException) {
|
||||
e.printStackTrace()
|
||||
stopRunner(false, e.message)
|
||||
}
|
||||
})
|
||||
+8
-16
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import androidx.room.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
@@ -62,13 +62,12 @@ class KeyValuePair() {
|
||||
@Deprecated("Use long.", ReplaceWith("long"))
|
||||
val int: Int?
|
||||
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
|
||||
val long: Long?
|
||||
get() = when (valueType) {
|
||||
@Suppress("DEPRECATION")
|
||||
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
|
||||
TYPE_LONG -> ByteBuffer.wrap(value).long
|
||||
else -> null
|
||||
}
|
||||
val long: Long? get() = when (valueType) {
|
||||
@Suppress("DEPRECATION")
|
||||
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
|
||||
TYPE_LONG -> ByteBuffer.wrap(value).long
|
||||
else -> null
|
||||
}
|
||||
val string: String?
|
||||
get() = if (valueType == TYPE_STRING) String(value) else null
|
||||
val stringSet: Set<String>?
|
||||
@@ -94,13 +93,11 @@ class KeyValuePair() {
|
||||
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: Float): KeyValuePair {
|
||||
valueType = TYPE_FLOAT
|
||||
this.value = ByteBuffer.allocate(4).putFloat(value).array()
|
||||
return this
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use long.")
|
||||
fun put(value: Int): KeyValuePair {
|
||||
@@ -108,26 +105,21 @@ class KeyValuePair() {
|
||||
this.value = ByteBuffer.allocate(4).putInt(value).array()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: Long): KeyValuePair {
|
||||
valueType = TYPE_LONG
|
||||
this.value = ByteBuffer.allocate(8).putLong(value).array()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: String): KeyValuePair {
|
||||
valueType = TYPE_STRING
|
||||
this.value = value.toByteArray()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: Set<String>): KeyValuePair {
|
||||
valueType = TYPE_STRING_SET
|
||||
val stream = ByteArrayOutputStream()
|
||||
val intBuffer = ByteBuffer.allocate(4)
|
||||
for (v in value) {
|
||||
intBuffer.rewind()
|
||||
stream.write(intBuffer.putInt(v.length).array())
|
||||
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
|
||||
stream.write(v.toByteArray())
|
||||
}
|
||||
this.value = stream.toByteArray()
|
||||
+26
-20
@@ -18,46 +18,52 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.github.shadowsocks.Core.app
|
||||
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
@Database(entities = [Profile::class, KeyValuePair::class], version = 1000)
|
||||
@Database(entities = [Profile::class, KeyValuePair::class], version = 29)
|
||||
abstract class PrivateDatabase : RoomDatabase() {
|
||||
companion object {
|
||||
private val instance by lazy {
|
||||
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE).apply {
|
||||
addMigrations(Migration1000)
|
||||
allowMainThreadQueries()
|
||||
enableMultiInstanceInvalidation()
|
||||
fallbackToDestructiveMigration()
|
||||
setQueryExecutor { GlobalScope.launch { it.run() } }
|
||||
}.build()
|
||||
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
|
||||
.addMigrations(
|
||||
Migration26,
|
||||
Migration27,
|
||||
Migration28
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
|
||||
val profileDao get() = instance.profileDao()
|
||||
val kvPairDao get() = instance.keyValuePairDao()
|
||||
}
|
||||
|
||||
abstract fun profileDao(): Profile.Dao
|
||||
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||
|
||||
object Migration1000 : RecreateSchemaMigration(999,
|
||||
1000,
|
||||
"Profile",
|
||||
"(`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`") {
|
||||
object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
|
||||
"(`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)",
|
||||
"`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
super.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
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -18,16 +18,14 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import android.database.sqlite.SQLiteCantOpenDatabaseException
|
||||
import android.util.LongSparseArray
|
||||
import com.github.shadowsocks.Core
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.forEachTry
|
||||
import com.github.shadowsocks.utils.printLog
|
||||
import com.google.gson.JsonStreamParser
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.json.JSONArray
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -43,7 +41,6 @@ object ProfileManager {
|
||||
fun onRemove(profileId: Long)
|
||||
fun onCleared()
|
||||
}
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
@Throws(SQLException::class)
|
||||
@@ -61,8 +58,9 @@ object ProfileManager {
|
||||
profiles?.values?.singleOrNull { it.id == DataStore.profileId }
|
||||
} else Core.currentProfile?.first
|
||||
val lazyClear = lazy { clear() }
|
||||
jsons.asIterable().forEachTry { json ->
|
||||
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) {
|
||||
var result: Exception? = null
|
||||
for (json in jsons) try {
|
||||
Profile.parseJson(json.bufferedReader().readText(), feature) {
|
||||
if (replace) {
|
||||
lazyClear.value
|
||||
// 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)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (result == null) result = e else result.addSuppressed(e)
|
||||
}
|
||||
if (result != null) throw result
|
||||
}
|
||||
|
||||
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
|
||||
if (profiles == null) return null
|
||||
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
|
||||
@@ -99,7 +99,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
@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)
|
||||
fun delProfile(id: Long) {
|
||||
+14
-20
@@ -18,39 +18,33 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.github.shadowsocks.Core
|
||||
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
@Database(entities = [KeyValuePair::class], version = 3)
|
||||
@Database(entities = [KeyValuePair::class], version = 4)
|
||||
abstract class PublicDatabase : RoomDatabase() {
|
||||
companion object {
|
||||
private val instance by lazy {
|
||||
Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
|
||||
.apply {
|
||||
addMigrations(Migration3)
|
||||
allowMainThreadQueries()
|
||||
enableMultiInstanceInvalidation()
|
||||
fallbackToDestructiveMigration()
|
||||
setQueryExecutor { GlobalScope.launch { it.run() } }
|
||||
}.build()
|
||||
.allowMainThreadQueries()
|
||||
.addMigrations(
|
||||
Migration3
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
val kvPairDao get() = instance.keyValuePairDao()
|
||||
}
|
||||
|
||||
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||
|
||||
internal object Migration3 : RecreateSchemaMigration(2,
|
||||
3,
|
||||
"KeyValuePair",
|
||||
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"`key`, `valueType`, `value`")
|
||||
internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
|
||||
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"`key`, `valueType`, `value`")
|
||||
}
|
||||
+3
-3
@@ -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.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
|
||||
private val schema: String, private val keys: String)
|
||||
: Migration(oldVersion, newVersion) {
|
||||
private val schema: String, private val keys: String) :
|
||||
Migration(oldVersion, newVersion) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE `tmp` $schema")
|
||||
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")
|
||||
+12
-14
@@ -18,10 +18,10 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.os.Build
|
||||
import com.github.shadowsocks.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
@@ -44,7 +44,7 @@ class ChannelMonitor : Thread("ChannelMonitor") {
|
||||
private var running = true
|
||||
|
||||
private fun registerInternal(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit) =
|
||||
channel.register(selector, ops, block)
|
||||
channel.register(selector, ops, block)
|
||||
|
||||
init {
|
||||
registrationPipe.source().apply {
|
||||
@@ -70,7 +70,7 @@ class ChannelMonitor : Thread("ChannelMonitor") {
|
||||
* Prevent NetworkOnMainThreadException because people enable strict mode for no reasons.
|
||||
*/
|
||||
private suspend fun WritableByteChannel.writeCompat(src: ByteBuffer) =
|
||||
if (Build.VERSION.SDK_INT <= 23) withContext(Dispatchers.Default) { write(src) } else write(src)
|
||||
if (Build.VERSION.SDK_INT <= 23) withContext(Dispatchers.Default) { write(src) } else write(src)
|
||||
|
||||
suspend fun register(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit): SelectionKey {
|
||||
val registration = Registration(channel, ops, block)
|
||||
@@ -86,17 +86,15 @@ class ChannelMonitor : Thread("ChannelMonitor") {
|
||||
return registration.result.await()
|
||||
}
|
||||
|
||||
suspend fun wait(channel: SelectableChannel, ops: Int) =
|
||||
CompletableDeferred<SelectionKey>().run {
|
||||
register(channel, ops) {
|
||||
if (it.isValid) try {
|
||||
it.interestOps(0) // stop listening
|
||||
} catch (_: CancelledKeyException) {
|
||||
}
|
||||
complete(it)
|
||||
}
|
||||
await()
|
||||
suspend fun wait(channel: SelectableChannel, ops: Int) = CompletableDeferred<SelectionKey>().run {
|
||||
register(channel, ops) {
|
||||
if (it.isValid) try {
|
||||
it.interestOps(0) // stop listening
|
||||
} catch (_: CancelledKeyException) { }
|
||||
complete(it)
|
||||
}
|
||||
await()
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
while (running) {
|
||||
+5
-6
@@ -18,17 +18,16 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.net.LocalSocket
|
||||
import com.github.shadowsocks.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) :
|
||||
LocalSocketListener(name, socketFile), CoroutineScope {
|
||||
override val coroutineContext =
|
||||
Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
launch { super.accept(socket) }
|
||||
+49
-40
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.net.ConnectivityManager
|
||||
@@ -26,12 +26,11 @@ import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import com.github.shadowsocks.Core
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.actor
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.UnknownHostException
|
||||
|
||||
object DefaultNetworkListener {
|
||||
@@ -40,14 +39,13 @@ object DefaultNetworkListener {
|
||||
class Get : NetworkMessage() {
|
||||
val response = CompletableDeferred<Network>()
|
||||
}
|
||||
|
||||
class Stop(val key: Any) : NetworkMessage()
|
||||
|
||||
class Put(val network: Network) : NetworkMessage()
|
||||
class Update(val network: Network) : NetworkMessage()
|
||||
class Lost(val network: Network) : NetworkMessage()
|
||||
}
|
||||
|
||||
@ObsoleteCoroutinesApi
|
||||
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
|
||||
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
||||
var network: Network? = null
|
||||
@@ -63,7 +61,7 @@ object DefaultNetworkListener {
|
||||
if (network == null) pendingRequests += message else message.response.complete(network)
|
||||
}
|
||||
is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
|
||||
listeners.remove(message.key) != null && listeners.isEmpty()) {
|
||||
listeners.remove(message.key) != null && listeners.isEmpty()) {
|
||||
network = null
|
||||
unregister()
|
||||
}
|
||||
@@ -74,9 +72,7 @@ object DefaultNetworkListener {
|
||||
pendingRequests.clear()
|
||||
listeners.values.forEach { it(network) }
|
||||
}
|
||||
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach {
|
||||
it(network)
|
||||
}
|
||||
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach { it(network) }
|
||||
is NetworkMessage.Lost -> if (network == message.network) {
|
||||
network = null
|
||||
listeners.values.forEach { it(null) }
|
||||
@@ -84,39 +80,55 @@ object DefaultNetworkListener {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun start(key: Any, listener: (Network?) -> Unit) =
|
||||
networkActor.send(NetworkMessage.Start(key, listener))
|
||||
|
||||
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(DefaultNetworkListener.NetworkMessage.Start(key, listener))
|
||||
suspend fun get() = if (fallback) @TargetApi(23) {
|
||||
Core.connectivity.activeNetwork
|
||||
?: throw UnknownHostException() // failed to listen, return current if available
|
||||
} else NetworkMessage.Get().run {
|
||||
connectivity.activeNetwork ?: throw UnknownHostException() // failed to listen, return current if available
|
||||
} else DefaultNetworkListener.NetworkMessage.Get().run {
|
||||
networkActor.send(this)
|
||||
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
|
||||
// private object Callback : ConnectivityManager.NetworkCallback() {
|
||||
// override fun onAvailable(network: Network) =
|
||||
// runBlocking { networkActor.send(NetworkMessage.Put(network)) }
|
||||
//
|
||||
// override fun onAvailable(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
|
||||
// override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
||||
// // 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(NetworkMessage.Lost(network)) }
|
||||
// override fun onLost(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network)) }
|
||||
// }
|
||||
|
||||
private var fallback = false
|
||||
private val connectivity = app.getSystemService<ConnectivityManager>()!!
|
||||
private val request = NetworkRequest.Builder().apply {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
}.build()
|
||||
|
||||
/**
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||
* 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
|
||||
*/
|
||||
private fun register() {
|
||||
// if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
||||
// Core.connectivity.registerDefaultNetworkCallback(Callback)
|
||||
// } else try {
|
||||
// fallback = false
|
||||
// // we want REQUEST here instead of LISTEN
|
||||
// Core.connectivity.requestNetwork(request, Callback)
|
||||
// } catch (e: SecurityException) {
|
||||
// // known bug: https://stackoverflow.com/a/33509180/2245107
|
||||
// // if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e)
|
||||
// fallback = true
|
||||
// }
|
||||
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
||||
connectivity.registerDefaultNetworkCallback(Callback)
|
||||
} else try {
|
||||
fallback = false
|
||||
// we want REQUEST here instead of LISTEN
|
||||
connectivity.requestNetwork(request, Callback)
|
||||
} catch (e: SecurityException) {
|
||||
fallback = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregister() {}//= Core.connectivity.unregisterNetworkCallback(Callback)
|
||||
private fun unregister() = connectivity.unregisterNetworkCallback(Callback)
|
||||
}
|
||||
+26
-23
@@ -18,21 +18,23 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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 com.github.shadowsocks.utils.useCancellable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.disconnectFromMain
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
@@ -42,21 +44,17 @@ import java.net.URLConnection
|
||||
class HttpsTest : ViewModel() {
|
||||
sealed class Status {
|
||||
protected abstract val status: CharSequence
|
||||
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) =
|
||||
setStatus(status)
|
||||
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) = setStatus(status)
|
||||
|
||||
object Idle : Status() {
|
||||
override val status get() = app.getText(R.string.vpn_connected)
|
||||
}
|
||||
|
||||
object Testing : Status() {
|
||||
override val status get() = app.getText(R.string.connection_test_testing)
|
||||
}
|
||||
|
||||
class Success(private val elapsed: Long) : Status() {
|
||||
override val status get() = app.getString(R.string.connection_test_available, elapsed)
|
||||
}
|
||||
|
||||
sealed class Error : Status() {
|
||||
override val status get() = app.getText(R.string.connection_test_fail)
|
||||
protected abstract val error: String
|
||||
@@ -71,43 +69,48 @@ class HttpsTest : ViewModel() {
|
||||
class UnexpectedResponseCode(private val code: Int) : Error() {
|
||||
override val error get() = app.getString(R.string.connection_test_error_status_code, code)
|
||||
}
|
||||
|
||||
class IOFailure(private val e: IOException) : Error() {
|
||||
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 }
|
||||
|
||||
fun testConnection() {
|
||||
cancelTest()
|
||||
status.value = Status.Testing
|
||||
val url = URL("https", "www.google.com", "/generate_204")
|
||||
val conn = (url.openConnection()) as HttpURLConnection
|
||||
val url = URL("https", when ((Core.currentProfile ?: return).first.route) {
|
||||
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.instanceFollowRedirects = false
|
||||
conn.useCaches = false
|
||||
running = GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
status.value = conn.useCancellable {
|
||||
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
status.value = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
val code = responseCode
|
||||
val code = conn.responseCode
|
||||
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)
|
||||
} catch (e: IOException) {
|
||||
Status.Error.IOFailure(e)
|
||||
} finally {
|
||||
disconnect()
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelTest() {
|
||||
running?.cancel()
|
||||
private fun cancelTest() = running?.let { (conn, job) ->
|
||||
job.cancel() // ensure job is cancelled before interrupting
|
||||
conn.disconnectFromMain()
|
||||
running = null
|
||||
}
|
||||
|
||||
+20
-42
@@ -18,10 +18,9 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import kotlinx.coroutines.*
|
||||
import org.xbill.DNS.*
|
||||
import java.io.IOException
|
||||
@@ -30,6 +29,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.channels.DatagramChannel
|
||||
import java.nio.channels.SelectionKey
|
||||
import java.nio.channels.SocketChannel
|
||||
import org.amnezia.vpn.R
|
||||
|
||||
/**
|
||||
* A simple DNS conditional forwarder.
|
||||
@@ -41,9 +41,7 @@ import java.nio.channels.SocketChannel
|
||||
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
|
||||
*/
|
||||
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
|
||||
private val remoteDns: Socks5Endpoint,
|
||||
private val proxy: SocketAddress,
|
||||
private val hosts: HostsFile) : CoroutineScope {
|
||||
private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope {
|
||||
/**
|
||||
* 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())
|
||||
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()
|
||||
|
||||
override val coroutineContext =
|
||||
SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||
|
||||
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
|
||||
configureBlocking(false)
|
||||
try {
|
||||
socket().bind(listen)
|
||||
} catch (e: BindException) {
|
||||
throw BaseService.ExpectedExceptionWrapper(e)
|
||||
}
|
||||
socket().bind(listen)
|
||||
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
|
||||
}
|
||||
|
||||
@@ -111,27 +93,20 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
|
||||
val request = try {
|
||||
Message(packet)
|
||||
} 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 supervisorScope {
|
||||
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
|
||||
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
|
||||
if (question?.type != Type.A) return@supervisorScope remote.await()
|
||||
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()
|
||||
val localResults = try {
|
||||
withTimeout(TIMEOUT) { localResolver(host) }
|
||||
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
// Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving")
|
||||
return@supervisorScope remote.await()
|
||||
} catch (_: UnknownHostException) {
|
||||
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 (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
|
||||
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()
|
||||
} catch (e: Exception) {
|
||||
remote.cancel()
|
||||
when (e) {
|
||||
// is TimeoutCancellationException -> Crashlytics.log(Log.WARN, TAG, "Remote resolving timed out")
|
||||
is CancellationException -> {
|
||||
} // ignore
|
||||
// is IOException -> Crashlytics.log(Log.WARN, TAG, e.message)
|
||||
is CancellationException -> { } // ignore
|
||||
else -> printLog(e)
|
||||
}
|
||||
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 {
|
||||
packet.position(0) // the packet might have been parsed, reset to beginning
|
||||
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.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE)
|
||||
val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE)
|
||||
remoteDns.tcpUnwrap(result, channel::read) {
|
||||
monitor.wait(channel, SelectionKey.OP_READ)
|
||||
}
|
||||
remoteDns.tcpUnwrap(result, channel::read) { monitor.wait(channel, SelectionKey.OP_READ) }
|
||||
result
|
||||
} else DatagramChannel.open().use { channel ->
|
||||
channel.configureBlocking(false)
|
||||
+2
-3
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.net.LocalServerSocket
|
||||
import android.net.LocalSocket
|
||||
@@ -26,7 +26,7 @@ import android.net.LocalSocketAddress
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
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.channels.Channel
|
||||
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.
|
||||
*/
|
||||
protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
|
||||
|
||||
protected abstract fun acceptInternal(socket: LocalSocket)
|
||||
final override fun run() {
|
||||
localSocket.use {
|
||||
+21
-30
@@ -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.Socks5Message
|
||||
import java.io.EOFException
|
||||
@@ -32,13 +32,12 @@ import kotlin.math.max
|
||||
|
||||
class Socks5Endpoint(host: String, port: Int) {
|
||||
private val dest = host.parseNumericAddress().let { numeric ->
|
||||
val bytes = numeric?.address
|
||||
?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
|
||||
val bytes = numeric?.address ?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
|
||||
val type = when (numeric) {
|
||||
null -> Socks5Message.SOCKS_ATYP_DOMAINNAME
|
||||
is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4
|
||||
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 {
|
||||
put(type.toByte())
|
||||
@@ -66,35 +65,34 @@ class Socks5Endpoint(host: String, port: Int) {
|
||||
flip()
|
||||
}
|
||||
}
|
||||
|
||||
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
|
||||
@ExperimentalUnsignedTypes
|
||||
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
|
||||
suspend fun readBytes(till: Int) {
|
||||
if (buffer.position() >= till) return
|
||||
while (reader(buffer) >= 0 && buffer.position() < till) wait()
|
||||
if (buffer.position() < till) throw EOFException("${buffer.position()} < $till")
|
||||
}
|
||||
|
||||
suspend fun read(index: Int): Byte {
|
||||
readBytes(index + 1)
|
||||
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(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]}")
|
||||
val dataOffset = when (val type = read(5)) {
|
||||
val dataOffset = when (read(5)) {
|
||||
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
||||
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
|
||||
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
||||
else -> throw IOException("Unsupported address type $type")
|
||||
else -> throw IllegalStateException("Unsupported address type ${buffer[5]}")
|
||||
} + 8
|
||||
readBytes(dataOffset + 2)
|
||||
buffer.limit(buffer.position()) // store old position to update mark
|
||||
buffer.position(dataOffset)
|
||||
val dataLength = buffer.short.toUShort().toInt()
|
||||
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.position(buffer.limit()) // restore old position
|
||||
buffer.limit(end)
|
||||
@@ -102,30 +100,23 @@ class Socks5Endpoint(host: String, port: Int) {
|
||||
buffer.reset()
|
||||
}
|
||||
|
||||
private fun ByteBuffer.tryPosition(newPosition: Int) {
|
||||
if (limit() < newPosition) throw EOFException("${limit()} < $newPosition")
|
||||
position(newPosition)
|
||||
fun udpWrap(packet: ByteBuffer) = ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
|
||||
// header
|
||||
putShort(0) // reserved
|
||||
put(0) // fragment number
|
||||
put(dest)
|
||||
// data
|
||||
put(packet)
|
||||
flip()
|
||||
}
|
||||
|
||||
fun udpWrap(packet: ByteBuffer) =
|
||||
ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
|
||||
// header
|
||||
putShort(0) // reserved
|
||||
put(0) // fragment number
|
||||
put(dest)
|
||||
// data
|
||||
put(packet)
|
||||
flip()
|
||||
}
|
||||
|
||||
fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
|
||||
fun udpUnwrap(packet: ByteBuffer) {
|
||||
packet.tryPosition(3)
|
||||
packet.tryPosition(6 + when (val type = packet.get()) {
|
||||
packet.position(3)
|
||||
packet.position(6 + when (packet.get()) {
|
||||
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
||||
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
|
||||
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
||||
else -> throw IOException("Unsupported address type $type")
|
||||
else -> throw IllegalStateException("Unsupported address type")
|
||||
})
|
||||
packet.mark()
|
||||
}
|
||||
+4
-5
@@ -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.util.*
|
||||
|
||||
@@ -42,7 +42,7 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
|
||||
private val addressLength get() = address.address.size shl 3
|
||||
|
||||
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 {
|
||||
@@ -61,7 +61,7 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
|
||||
if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
|
||||
|
||||
private fun Byte.unsigned() = toInt() and 0xFF
|
||||
override fun compareTo(other: Subnet): Int {
|
||||
@@ -80,6 +80,5 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
|
||||
val that = other as? Subnet
|
||||
return address == that?.address && prefixSize == that.prefixSize
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(address, prefixSize)
|
||||
}
|
||||
+10
-13
@@ -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.withTimeoutOrNull
|
||||
import java.io.File
|
||||
@@ -34,8 +34,7 @@ object TcpFastOpen {
|
||||
*/
|
||||
val supported by lazy {
|
||||
if (File(PATH).canRead()) return@lazy true
|
||||
val match =
|
||||
"""^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
|
||||
val match = """^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
|
||||
if (match == null) false else when (match.groupValues[1].toInt()) {
|
||||
in Int.MIN_VALUE..2 -> false
|
||||
3 -> when (match.groupValues[2].toInt()) {
|
||||
@@ -47,22 +46,20 @@ object TcpFastOpen {
|
||||
}
|
||||
}
|
||||
|
||||
val sendEnabled: Boolean
|
||||
get() {
|
||||
val file = File(PATH)
|
||||
// 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
|
||||
return if (file.canRead()) file.bufferedReader().use { it.readText() }.trim().toInt() and 1 > 0 else supported
|
||||
}
|
||||
val sendEnabled: Boolean get() {
|
||||
val file = File(PATH)
|
||||
// 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
|
||||
return if (file.canRead()) file.bufferedReader().use { it.readText() }.trim().toInt() and 1 > 0 else supported
|
||||
}
|
||||
|
||||
fun enable(): String? {
|
||||
return try {
|
||||
ProcessBuilder("su", "-c", "echo 3 > $PATH").redirectErrorStream(true).start()
|
||||
.inputStream.bufferedReader().readText()
|
||||
.inputStream.bufferedReader().readText()
|
||||
} catch (e: IOException) {
|
||||
e.readableMessage
|
||||
}
|
||||
}
|
||||
|
||||
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
|
||||
}
|
||||
+9
-16
@@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* 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 *
|
||||
@@ -18,23 +18,16 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.net
|
||||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import com.github.shadowsocks.utils.computeIfAbsentCompat
|
||||
import com.github.shadowsocks.utils.parseNumericAddress
|
||||
import java.net.InetAddress
|
||||
|
||||
class HostsFile(input: String = "") {
|
||||
private val map = mutableMapOf<String, MutableSet<InetAddress>>()
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.os.Bundle
|
||||
|
||||
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
|
||||
init {
|
||||
for (line in input.lineSequence()) {
|
||||
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)
|
||||
}
|
||||
check(resolveInfo.providerInfo != null)
|
||||
}
|
||||
|
||||
val configuredHostnames get() = map.size
|
||||
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList()
|
||||
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
|
||||
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)
|
||||
}
|
||||
}
|
||||
+53
-12
@@ -18,18 +18,20 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.preference
|
||||
package org.amnezia.vpn.shadowsocks.core.preference
|
||||
|
||||
import android.os.Binder
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.Core
|
||||
import com.github.shadowsocks.database.PrivateDatabase
|
||||
import com.github.shadowsocks.database.PublicDatabase
|
||||
import com.github.shadowsocks.net.TcpFastOpen
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.parsePort
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.database.PrivateDatabase
|
||||
import org.amnezia.vpn.shadowsocks.core.database.PublicDatabase
|
||||
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
|
||||
object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
|
||||
@@ -40,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
publicStore.registerChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
|
||||
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
|
||||
when (key) {
|
||||
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
|
||||
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
|
||||
|
||||
private fun getLocalPort(key: String, default: Int): Int {
|
||||
val value = publicStore.getInt(key)
|
||||
return if (value != null) {
|
||||
@@ -62,8 +63,29 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
set(value) = publicStore.putLong(Key.id, value)
|
||||
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
|
||||
val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
|
||||
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, false)
|
||||
val listenAddress get() = "127.0.0.1"
|
||||
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, true)
|
||||
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
|
||||
get() = getLocalPort(Key.portProxy, 1080)
|
||||
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
||||
@@ -71,6 +93,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
var portLocalDns: Int
|
||||
get() = getLocalPort(Key.portLocalDns, 5450)
|
||||
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.
|
||||
@@ -79,11 +104,27 @@ object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
|
||||
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
|
||||
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
||||
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||
}
|
||||
|
||||
var editingId: Long?
|
||||
get() = privateStore.getLong(Key.id)
|
||||
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
|
||||
get() = privateStore.getBoolean(Key.dirty) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.dirty, value)
|
||||
+2
-2
@@ -18,10 +18,10 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.preference
|
||||
package org.amnezia.vpn.shadowsocks.core.preference
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
interface OnPreferenceDataStoreChangeListener {
|
||||
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
|
||||
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?)
|
||||
}
|
||||
+15
-34
@@ -18,15 +18,14 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.preference
|
||||
package org.amnezia.vpn.shadowsocks.core.preference
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.database.KeyValuePair
|
||||
import java.util.*
|
||||
import org.amnezia.vpn.shadowsocks.core.database.KeyValuePair
|
||||
import java.util.HashSet
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
||||
PreferenceDataStore() {
|
||||
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() {
|
||||
fun getBoolean(key: String) = kvPairDao[key]?.boolean
|
||||
fun getFloat(key: String) = kvPairDao[key]?.float
|
||||
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
|
||||
@@ -39,49 +38,36 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
||||
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
|
||||
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
|
||||
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
|
||||
override fun getStringSet(key: String, defValue: MutableSet<String>?) =
|
||||
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())
|
||||
override fun getStringSet(key: String, defValue: MutableSet<String>?) = 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 putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||
override fun putBoolean(key: String, value: Boolean) {
|
||||
kvPairDao.put(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float) {
|
||||
kvPairDao.put(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int) {
|
||||
kvPairDao.put(KeyValuePair(key).put(value.toLong()))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long) {
|
||||
kvPairDao.put(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
|
||||
kvPairDao.put(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String, values: MutableSet<String>?) =
|
||||
if (values == null) remove(key) else {
|
||||
kvPairDao.put(KeyValuePair(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putStringSet(key: String, values: MutableSet<String>?) = if (values == null) remove(key) else {
|
||||
kvPairDao.put(KeyValuePair(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
kvPairDao.delete(key)
|
||||
@@ -89,12 +75,7 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
||||
}
|
||||
|
||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||
private fun fireChangeListener(key: String) =
|
||||
listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) =
|
||||
listeners.add(listener)
|
||||
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) =
|
||||
listeners.remove(listener)
|
||||
private fun fireChangeListener(key: String) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
|
||||
}
|
||||
+8
-3
@@ -18,10 +18,11 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.utils
|
||||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import org.json.JSONArray
|
||||
|
||||
private sealed class ArrayIterator<out T> : Iterator<T> {
|
||||
abstract val size: Int
|
||||
@@ -35,12 +36,16 @@ private class ClipDataIterator(private val data: ClipData) : ArrayIterator<ClipD
|
||||
override val size get() = data.itemCount
|
||||
override fun get(index: Int) = data.getItemAt(index)
|
||||
}
|
||||
|
||||
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>() {
|
||||
override val size get() = list.size()
|
||||
override fun get(index: Int) = list[index]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
+29
-6
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.utils
|
||||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
object Key {
|
||||
/**
|
||||
@@ -30,13 +30,27 @@ object Key {
|
||||
const val id = "profileId"
|
||||
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 portLocalDns = "portLocalDns"
|
||||
const val portTransproxy = "portTransproxy"
|
||||
|
||||
const val route = "route"
|
||||
|
||||
const val isAutoConnect = "isAutoConnect"
|
||||
const val directBootAware = "directBootAware"
|
||||
|
||||
const val proxyApps = "isProxyApps"
|
||||
const val bypass = "isBypassApps"
|
||||
const val udpdns = "isUdpDns"
|
||||
const val ipv6 = "isIpv6"
|
||||
const val metered = "metered"
|
||||
|
||||
const val host = "proxy"
|
||||
const val password = "sitekey"
|
||||
@@ -44,17 +58,26 @@ object Key {
|
||||
const val remotePort = "remotePortNum"
|
||||
const val remoteDns = "remoteDns"
|
||||
|
||||
const val plugin = "plugin"
|
||||
const val pluginConfigure = "plugin.configure"
|
||||
const val udpFallback = "udpFallback"
|
||||
|
||||
const val dirty = "profileDirty"
|
||||
|
||||
const val tfo = "tcp_fastopen"
|
||||
const val hosts = "hosts"
|
||||
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 {
|
||||
const val SERVICE = "com.github.shadowsocks.SERVICE"
|
||||
const val CLOSE = "com.github.shadowsocks.CLOSE"
|
||||
const val RELOAD = "com.github.shadowsocks.RELOAD"
|
||||
const val SERVICE = "com.kyle.shadowsocks.SERVICE"
|
||||
const val CLOSE = "com.kyle.shadowsocks.CLOSE"
|
||||
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"
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.utils
|
||||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
+10
-15
@@ -1,16 +1,16 @@
|
||||
package com.github.shadowsocks.utils
|
||||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.github.shadowsocks.Core
|
||||
import com.github.shadowsocks.Core.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
@@ -23,9 +23,7 @@ object DirectBoot : BroadcastReceiver() {
|
||||
|
||||
fun getDeviceProfile(): Pair<Profile, Profile?>? = try {
|
||||
ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> }
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
} catch (_: IOException) { null }
|
||||
|
||||
fun clean() {
|
||||
file.delete()
|
||||
@@ -37,10 +35,8 @@ object DirectBoot : BroadcastReceiver() {
|
||||
* app.currentProfile will call this.
|
||||
*/
|
||||
fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) =
|
||||
if (profile == null) clean()
|
||||
else ObjectOutputStream(file.outputStream()).use {
|
||||
it.writeObject(ProfileManager.expand(profile))
|
||||
}
|
||||
if (profile == null) clean()
|
||||
else ObjectOutputStream(file.outputStream()).use { it.writeObject(ProfileManager.expand(profile)) }
|
||||
|
||||
fun flushTrafficStats() {
|
||||
getDeviceProfile()?.also { (profile, fallback) ->
|
||||
@@ -55,7 +51,6 @@ object DirectBoot : BroadcastReceiver() {
|
||||
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
|
||||
registered = true
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
flushTrafficStats()
|
||||
app.unregisterReceiver(this)
|
||||
+17
-55
@@ -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.ContentResolver
|
||||
import android.content.Context
|
||||
@@ -36,64 +35,30 @@ import android.system.OsConstants
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.preference.Preference
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.net.HttpURLConnection
|
||||
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
|
||||
|
||||
private val parseNumericAddress by lazy @SuppressLint("DiscouragedPrivateApi") {
|
||||
private val parseNumericAddress by lazy {
|
||||
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
||||
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? =
|
||||
Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let {
|
||||
if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null,
|
||||
this) as InetAddress
|
||||
}
|
||||
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
|
||||
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
|
||||
|
||||
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) =
|
||||
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() }
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
cont.resume(block())
|
||||
} catch (e: Throwable) {
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun HttpURLConnection.disconnectFromMain() {
|
||||
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
|
||||
}
|
||||
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
|
||||
}
|
||||
|
||||
fun ContentResolver.openBitmap(uri: Uri) =
|
||||
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
|
||||
else BitmapFactory.decodeStream(openInputStream(uri))
|
||||
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
|
||||
else BitmapFactory.decodeStream(openInputStream(uri))
|
||||
|
||||
val PackageInfo.signaturesCompat
|
||||
get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
|
||||
val PackageInfo.signaturesCompat get() =
|
||||
if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
|
||||
|
||||
/**
|
||||
* Based on: https://stackoverflow.com/a/26348729/2245107
|
||||
@@ -122,11 +86,9 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
|
||||
return typedValue.resourceId
|
||||
}
|
||||
|
||||
val Intent.datas
|
||||
get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
|
||||
val Intent.datas get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
|
||||
|
||||
fun printLog(t: Throwable) {
|
||||
// Crashlytics.logException(t)
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user