Shadowsocks open source code added
This commit is contained in:
parent
4a6ea38ef8
commit
ccdd433e35
57 changed files with 4753 additions and 1 deletions
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.github.shadowsocks.aidl;
|
||||||
|
|
||||||
|
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback;
|
||||||
|
|
||||||
|
interface IShadowsocksService {
|
||||||
|
int getState();
|
||||||
|
String getProfileName();
|
||||||
|
|
||||||
|
void registerCallback(in IShadowsocksServiceCallback cb);
|
||||||
|
void startListeningForBandwidth(in IShadowsocksServiceCallback cb, long timeout);
|
||||||
|
oneway void stopListeningForBandwidth(in IShadowsocksServiceCallback cb);
|
||||||
|
oneway void unregisterCallback(in IShadowsocksServiceCallback cb);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.github.shadowsocks.aidl;
|
||||||
|
|
||||||
|
parcelable TrafficStats;
|
||||||
20
client/android/assets/acl/bypass-lan.acl
Normal file
20
client/android/assets/acl/bypass-lan.acl
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[proxy_all]
|
||||||
|
|
||||||
|
[bypass_list]
|
||||||
|
0.0.0.0/8
|
||||||
|
10.0.0.0/8
|
||||||
|
100.64.0.0/10
|
||||||
|
127.0.0.0/8
|
||||||
|
169.254.0.0/16
|
||||||
|
172.16.0.0/12
|
||||||
|
192.0.0.0/24
|
||||||
|
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
|
||||||
|
224.0.0.0/3
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext{
|
ext{
|
||||||
kotlin_version = "1.4.30-M1"
|
kotlin_version = "1.5.0"
|
||||||
// for libwg
|
// for libwg
|
||||||
appcompatVersion = '1.1.0'
|
appcompatVersion = '1.1.0'
|
||||||
annotationsVersion = '1.0.1'
|
annotationsVersion = '1.0.1'
|
||||||
|
|
@ -43,6 +43,15 @@ dependencies {
|
||||||
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||||
|
//ss
|
||||||
|
implementation "androidx.preference:preference:1.1.0"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:2.3.4"
|
||||||
|
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 "com.google.code.gson:gson:2.8.5"
|
||||||
|
implementation "org.connectbot.jsocks:jsocks:1.0.0"
|
||||||
|
annotationProcessor "androidx.room:room-compiler:2.3.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -88,6 +97,7 @@ android {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
abortOnError false
|
abortOnError false
|
||||||
|
|
|
||||||
0
client/android/gradlew
vendored
Normal file → Executable file
0
client/android/gradlew
vendored
Normal file → Executable file
10
client/android/res/drawable/ic_navigation_close.xml
Normal file
10
client/android/res/drawable/ic_navigation_close.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||||
|
</vector>
|
||||||
11
client/android/res/drawable/ic_service_active.xml
Normal file
11
client/android/res/drawable/ic_service_active.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:name="path"
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
|
||||||
|
</vector>
|
||||||
11
client/android/res/drawable/ic_service_busy.xml
Normal file
11
client/android/res/drawable/ic_service_busy.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:name="path"
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
|
||||||
|
</vector>
|
||||||
16
client/android/res/drawable/ic_service_connected.xml
Normal file
16
client/android/res/drawable/ic_service_connected.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:drawable="@drawable/ic_service_busy">
|
||||||
|
<target android:name="path">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="pathData"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:valueFrom="M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||||
|
android:valueTo="M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||||
|
android:valueType="pathType"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
||||||
27
client/android/res/drawable/ic_service_connecting.xml
Normal file
27
client/android/res/drawable/ic_service_connecting.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:drawable="@drawable/ic_service_idle">
|
||||||
|
<target android:name="strike_thru_path">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="pathData"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:valueFrom="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
|
||||||
|
android:valueTo="M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z"
|
||||||
|
android:valueType="pathType"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
<target android:name="strike_thru_mask">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="pathData"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:valueFrom="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"
|
||||||
|
android:valueTo="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z"
|
||||||
|
android:valueType="pathType"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
||||||
18
client/android/res/drawable/ic_service_idle.xml
Normal file
18
client/android/res/drawable/ic_service_idle.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:name="strike_thru_path"
|
||||||
|
android:pathData="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
<clip-path
|
||||||
|
android:name="strike_thru_mask"
|
||||||
|
android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z" />
|
||||||
|
<path
|
||||||
|
android:name="holey_icon"
|
||||||
|
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||||
|
android:fillColor="#fff" />
|
||||||
|
</vector>
|
||||||
27
client/android/res/drawable/ic_service_stopped.xml
Normal file
27
client/android/res/drawable/ic_service_stopped.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:drawable="@drawable/ic_service_idle">
|
||||||
|
<target android:name="strike_thru_path">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="pathData"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:valueFrom="M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z"
|
||||||
|
android:valueTo="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
|
||||||
|
android:valueType="pathType"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
<target android:name="strike_thru_mask">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="pathData"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:valueFrom="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z"
|
||||||
|
android:valueTo="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"
|
||||||
|
android:valueType="pathType"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
||||||
16
client/android/res/drawable/ic_service_stopping.xml
Normal file
16
client/android/res/drawable/ic_service_stopping.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:drawable="@drawable/ic_service_busy">
|
||||||
|
<target android:name="path">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="pathData"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:valueFrom="M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||||
|
android:valueTo="M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||||
|
android:valueType="pathType"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
||||||
11
client/android/res/drawable/ic_social_share.xml
Normal file
11
client/android/res/drawable/ic_social_share.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||||
|
</vector>
|
||||||
210
client/android/res/values/arrays.xml
Normal file
210
client/android/res/values/arrays.xml
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="add_first_profile">
|
||||||
|
<item>@string/add_profile_methods_scan_qr_code</item>
|
||||||
|
<item>@string/action_import_file</item>
|
||||||
|
<item>@string/add_profile_methods_manual_settings</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="enc_method_entry" translatable="false">
|
||||||
|
<item>RC4-MD5</item>
|
||||||
|
<item>AES-128-CFB</item>
|
||||||
|
<item>AES-192-CFB</item>
|
||||||
|
<item>AES-256-CFB</item>
|
||||||
|
<item>AES-128-CTR</item>
|
||||||
|
<item>AES-192-CTR</item>
|
||||||
|
<item>AES-256-CTR</item>
|
||||||
|
<item>BF-CFB</item>
|
||||||
|
<item>CAMELLIA-128-CFB</item>
|
||||||
|
<item>CAMELLIA-192-CFB</item>
|
||||||
|
<item>CAMELLIA-256-CFB</item>
|
||||||
|
<item>SALSA20</item>
|
||||||
|
<item>CHACHA20</item>
|
||||||
|
<item>CHACHA20-IETF</item>
|
||||||
|
<item>AES-128-GCM</item>
|
||||||
|
<item>AES-192-GCM</item>
|
||||||
|
<item>AES-256-GCM</item>
|
||||||
|
<item>CHACHA20-IETF-POLY1305</item>
|
||||||
|
<item>XCHACHA20-IETF-POLY1305</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="enc_method_value" translatable="false">
|
||||||
|
<item>rc4-md5</item>
|
||||||
|
<item>aes-128-cfb</item>
|
||||||
|
<item>aes-192-cfb</item>
|
||||||
|
<item>aes-256-cfb</item>
|
||||||
|
<item>aes-128-ctr</item>
|
||||||
|
<item>aes-192-ctr</item>
|
||||||
|
<item>aes-256-ctr</item>
|
||||||
|
<item>bf-cfb</item>
|
||||||
|
<item>camellia-128-cfb</item>
|
||||||
|
<item>camellia-192-cfb</item>
|
||||||
|
<item>camellia-256-cfb</item>
|
||||||
|
<item>salsa20</item>
|
||||||
|
<item>chacha20</item>
|
||||||
|
<item>chacha20-ietf</item>
|
||||||
|
<item>aes-128-gcm</item>
|
||||||
|
<item>aes-192-gcm</item>
|
||||||
|
<item>aes-256-gcm</item>
|
||||||
|
<item>chacha20-ietf-poly1305</item>
|
||||||
|
<item>xchacha20-ietf-poly1305</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="bypass_private_route" translatable="false">
|
||||||
|
<item>1.0.0.0/8</item>
|
||||||
|
<item>2.0.0.0/7</item>
|
||||||
|
<item>4.0.0.0/6</item>
|
||||||
|
<item>8.0.0.0/7</item>
|
||||||
|
<item>11.0.0.0/8</item>
|
||||||
|
<item>12.0.0.0/6</item>
|
||||||
|
<item>16.0.0.0/4</item>
|
||||||
|
<item>32.0.0.0/3</item>
|
||||||
|
<item>64.0.0.0/3</item>
|
||||||
|
<item>96.0.0.0/6</item>
|
||||||
|
<item>100.0.0.0/10</item>
|
||||||
|
<item>100.128.0.0/9</item>
|
||||||
|
<item>101.0.0.0/8</item>
|
||||||
|
<item>102.0.0.0/7</item>
|
||||||
|
<item>104.0.0.0/5</item>
|
||||||
|
<item>112.0.0.0/10</item>
|
||||||
|
<item>112.64.0.0/11</item>
|
||||||
|
<item>112.96.0.0/12</item>
|
||||||
|
<item>112.112.0.0/13</item>
|
||||||
|
<item>112.120.0.0/14</item>
|
||||||
|
<item>112.124.0.0/19</item>
|
||||||
|
<item>112.124.32.0/21</item>
|
||||||
|
<item>112.124.40.0/22</item>
|
||||||
|
<item>112.124.44.0/23</item>
|
||||||
|
<item>112.124.46.0/24</item>
|
||||||
|
<item>112.124.48.0/20</item>
|
||||||
|
<item>112.124.64.0/18</item>
|
||||||
|
<item>112.124.128.0/17</item>
|
||||||
|
<item>112.125.0.0/16</item>
|
||||||
|
<item>112.126.0.0/15</item>
|
||||||
|
<item>112.128.0.0/9</item>
|
||||||
|
<item>113.0.0.0/8</item>
|
||||||
|
<item>114.0.0.0/10</item>
|
||||||
|
<item>114.64.0.0/11</item>
|
||||||
|
<item>114.96.0.0/12</item>
|
||||||
|
<item>114.112.0.0/15</item>
|
||||||
|
<item>114.114.0.0/18</item>
|
||||||
|
<item>114.114.64.0/19</item>
|
||||||
|
<item>114.114.96.0/20</item>
|
||||||
|
<item>114.114.112.0/23</item>
|
||||||
|
<item>114.114.115.0/24</item>
|
||||||
|
<item>114.114.116.0/22</item>
|
||||||
|
<item>114.114.120.0/21</item>
|
||||||
|
<item>114.114.128.0/17</item>
|
||||||
|
<item>114.115.0.0/16</item>
|
||||||
|
<item>114.116.0.0/14</item>
|
||||||
|
<item>114.120.0.0/13</item>
|
||||||
|
<item>114.128.0.0/9</item>
|
||||||
|
<item>115.0.0.0/8</item>
|
||||||
|
<item>116.0.0.0/6</item>
|
||||||
|
<item>120.0.0.0/6</item>
|
||||||
|
<item>124.0.0.0/7</item>
|
||||||
|
<item>126.0.0.0/8</item>
|
||||||
|
<item>128.0.0.0/3</item>
|
||||||
|
<item>160.0.0.0/5</item>
|
||||||
|
<item>168.0.0.0/8</item>
|
||||||
|
<item>169.0.0.0/9</item>
|
||||||
|
<item>169.128.0.0/10</item>
|
||||||
|
<item>169.192.0.0/11</item>
|
||||||
|
<item>169.224.0.0/12</item>
|
||||||
|
<item>169.240.0.0/13</item>
|
||||||
|
<item>169.248.0.0/14</item>
|
||||||
|
<item>169.252.0.0/15</item>
|
||||||
|
<item>169.255.0.0/16</item>
|
||||||
|
<item>170.0.0.0/7</item>
|
||||||
|
<item>172.0.0.0/12</item>
|
||||||
|
<item>172.32.0.0/11</item>
|
||||||
|
<item>172.64.0.0/10</item>
|
||||||
|
<item>172.128.0.0/9</item>
|
||||||
|
<item>173.0.0.0/8</item>
|
||||||
|
<item>174.0.0.0/7</item>
|
||||||
|
<item>176.0.0.0/4</item>
|
||||||
|
<item>192.0.0.8/29</item>
|
||||||
|
<item>192.0.0.16/28</item>
|
||||||
|
<item>192.0.0.32/27</item>
|
||||||
|
<item>192.0.0.64/26</item>
|
||||||
|
<item>192.0.0.128/25</item>
|
||||||
|
<item>192.0.1.0/24</item>
|
||||||
|
<item>192.0.3.0/24</item>
|
||||||
|
<item>192.0.4.0/22</item>
|
||||||
|
<item>192.0.8.0/21</item>
|
||||||
|
<item>192.0.16.0/20</item>
|
||||||
|
<item>192.0.32.0/19</item>
|
||||||
|
<item>192.0.64.0/18</item>
|
||||||
|
<item>192.0.128.0/17</item>
|
||||||
|
<item>192.1.0.0/16</item>
|
||||||
|
<item>192.2.0.0/15</item>
|
||||||
|
<item>192.4.0.0/14</item>
|
||||||
|
<item>192.8.0.0/13</item>
|
||||||
|
<item>192.16.0.0/12</item>
|
||||||
|
<item>192.32.0.0/11</item>
|
||||||
|
<item>192.64.0.0/12</item>
|
||||||
|
<item>192.80.0.0/13</item>
|
||||||
|
<item>192.88.0.0/18</item>
|
||||||
|
<item>192.88.64.0/19</item>
|
||||||
|
<item>192.88.96.0/23</item>
|
||||||
|
<item>192.88.98.0/24</item>
|
||||||
|
<item>192.88.100.0/22</item>
|
||||||
|
<item>192.88.104.0/21</item>
|
||||||
|
<item>192.88.112.0/20</item>
|
||||||
|
<item>192.88.128.0/17</item>
|
||||||
|
<item>192.89.0.0/16</item>
|
||||||
|
<item>192.90.0.0/15</item>
|
||||||
|
<item>192.92.0.0/14</item>
|
||||||
|
<item>192.96.0.0/11</item>
|
||||||
|
<item>192.128.0.0/11</item>
|
||||||
|
<item>192.160.0.0/13</item>
|
||||||
|
<item>192.169.0.0/16</item>
|
||||||
|
<item>192.170.0.0/15</item>
|
||||||
|
<item>192.172.0.0/14</item>
|
||||||
|
<item>192.176.0.0/12</item>
|
||||||
|
<item>192.192.0.0/10</item>
|
||||||
|
<item>193.0.0.0/8</item>
|
||||||
|
<item>194.0.0.0/7</item>
|
||||||
|
<item>196.0.0.0/7</item>
|
||||||
|
<item>198.0.0.0/12</item>
|
||||||
|
<item>198.16.0.0/15</item>
|
||||||
|
<item>198.20.0.0/14</item>
|
||||||
|
<item>198.24.0.0/13</item>
|
||||||
|
<item>198.32.0.0/12</item>
|
||||||
|
<item>198.48.0.0/15</item>
|
||||||
|
<item>198.50.0.0/16</item>
|
||||||
|
<item>198.51.0.0/18</item>
|
||||||
|
<item>198.51.64.0/19</item>
|
||||||
|
<item>198.51.96.0/22</item>
|
||||||
|
<item>198.51.101.0/24</item>
|
||||||
|
<item>198.51.102.0/23</item>
|
||||||
|
<item>198.51.104.0/21</item>
|
||||||
|
<item>198.51.112.0/20</item>
|
||||||
|
<item>198.51.128.0/17</item>
|
||||||
|
<item>198.52.0.0/14</item>
|
||||||
|
<item>198.56.0.0/13</item>
|
||||||
|
<item>198.64.0.0/10</item>
|
||||||
|
<item>198.128.0.0/9</item>
|
||||||
|
<item>199.0.0.0/8</item>
|
||||||
|
<item>200.0.0.0/7</item>
|
||||||
|
<item>202.0.0.0/8</item>
|
||||||
|
<item>203.0.0.0/18</item>
|
||||||
|
<item>203.0.64.0/19</item>
|
||||||
|
<item>203.0.96.0/20</item>
|
||||||
|
<item>203.0.112.0/24</item>
|
||||||
|
<item>203.0.114.0/23</item>
|
||||||
|
<item>203.0.116.0/22</item>
|
||||||
|
<item>203.0.120.0/21</item>
|
||||||
|
<item>203.0.128.0/17</item>
|
||||||
|
<item>203.1.0.0/16</item>
|
||||||
|
<item>203.2.0.0/15</item>
|
||||||
|
<item>203.4.0.0/14</item>
|
||||||
|
<item>203.8.0.0/13</item>
|
||||||
|
<item>203.16.0.0/12</item>
|
||||||
|
<item>203.32.0.0/11</item>
|
||||||
|
<item>203.64.0.0/10</item>
|
||||||
|
<item>203.128.0.0/9</item>
|
||||||
|
<item>204.0.0.0/6</item>
|
||||||
|
<item>208.0.0.0/4</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
35
client/android/res/values/colors.xml
Normal file
35
client/android/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="background_selected">@color/material_primary_100</color>
|
||||||
|
<color name="background_stat">@color/material_primary_300</color>
|
||||||
|
<color name="ic_launcher_background">#7488A1</color>
|
||||||
|
|
||||||
|
<!-- ssplugin ============================================ -->
|
||||||
|
<color name="material_green_700">#388E3C</color>
|
||||||
|
<color name="material_green_a700">#00C853</color>
|
||||||
|
<color name="material_blue_grey_100">#CFD8DC</color>
|
||||||
|
<color name="material_blue_grey_300">#90A4AE</color>
|
||||||
|
<color name="material_blue_grey_500">#607D8B</color>
|
||||||
|
<color name="material_blue_grey_600">#546E7A</color>
|
||||||
|
<color name="material_blue_grey_700">#455A64</color>
|
||||||
|
<color name="material_primary_100">@color/material_blue_grey_100</color>
|
||||||
|
<color name="material_primary_300">@color/material_blue_grey_300</color>
|
||||||
|
<color name="material_primary_500">@color/material_blue_grey_500</color>
|
||||||
|
<color name="material_primary_600">@color/material_blue_grey_600</color>
|
||||||
|
<color name="material_primary_700">@color/material_blue_grey_700</color>
|
||||||
|
<color name="material_primary_800">@color/material_blue_grey_800</color>
|
||||||
|
<color name="material_primary_900">@color/material_blue_grey_900</color>
|
||||||
|
<color name="material_accent_200">@color/material_green_a700</color>
|
||||||
|
|
||||||
|
<color name="light_color_primary">@color/material_primary_500</color>
|
||||||
|
<color name="light_color_primary_dark">@color/material_primary_700</color>
|
||||||
|
<color name="light_color_primary_text">@color/material_primary_500</color>
|
||||||
|
<color name="dark_color_primary">@color/material_primary_800</color>
|
||||||
|
<color name="dark_color_primary_dark">@color/material_primary_900</color>
|
||||||
|
<color name="dark_color_primary_text">@color/material_primary_300</color>
|
||||||
|
|
||||||
|
<color name="color_primary">@color/light_color_primary</color>
|
||||||
|
<color name="color_primary_dark">@color/light_color_primary_dark</color>
|
||||||
|
<color name="color_primary_text">@color/light_color_primary_text</color>
|
||||||
|
|
||||||
|
</resources>
|
||||||
7
client/android/res/values/dimen.xml
Normal file
7
client/android/res/values/dimen.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="qr_code_size">250dp</dimen>
|
||||||
|
<dimen name="profile_padding">8dp</dimen>
|
||||||
|
<dimen name="main_list_padding_bottom">88dp</dimen>
|
||||||
|
<dimen name="bottom_sheet_padding">8dp</dimen>
|
||||||
|
</resources>
|
||||||
113
client/android/res/values/strings.xml
Normal file
113
client/android/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Shadowsocks</string>
|
||||||
|
<string name="send_email">Send email</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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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">Sent</string>
|
||||||
|
<string name="received">Received</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="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>
|
||||||
165
client/android/src/com/github/shadowsocks/Core.kt
Normal file
165
client/android/src/com/github/shadowsocks/Core.kt
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2018 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
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
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
|
||||||
|
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 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
|
||||||
|
|
||||||
|
object Core {
|
||||||
|
const val TAG = "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
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeProfileIds
|
||||||
|
get() = ProfileManager.getProfile(DataStore.profileId).let {
|
||||||
|
if (it == null) emptyList() else listOfNotNull(it.id)
|
||||||
|
}
|
||||||
|
val currentProfile: Pair<Profile, Profile?>?
|
||||||
|
get() {
|
||||||
|
if (DataStore.directBootAware) DirectBoot.getDeviceProfile()?.apply { return this }
|
||||||
|
return ProfileManager.expand(ProfileManager.getProfile(DataStore.profileId)
|
||||||
|
?: return null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchProfile(id: Long): Profile {
|
||||||
|
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
|
||||||
|
DataStore.profileId = result.id
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||||
|
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// handle data restored/crash
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
printLog(e)
|
||||||
|
}
|
||||||
|
DataStore.publicStore.putLong(Key.assetUpdateTime, packageInfo.lastUpdateTime)
|
||||||
|
}
|
||||||
|
updateNotificationChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
client/android/src/com/github/shadowsocks/LocalVpnService.kt
Normal file
215
client/android/src/com/github/shadowsocks/LocalVpnService.kt
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.aidl
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
companion object {
|
||||||
|
val serviceClass = LocalVpnService::class.java
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
||||||
|
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() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var connectionActive = false
|
||||||
|
private var callbackRegistered = false
|
||||||
|
private var callback: Callback? = null
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||||
|
val callback = callback ?: return
|
||||||
|
handler.post { callback.trafficUpdated(profileId, stats) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun trafficPersisted(profileId: Long) {
|
||||||
|
val callback = callback ?: return
|
||||||
|
handler.post { callback.trafficPersisted(profileId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var binder: IBinder? = null
|
||||||
|
|
||||||
|
var bandwidthTimeout = 0L
|
||||||
|
set(value) {
|
||||||
|
try {
|
||||||
|
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)
|
||||||
|
else service?.stopListeningForBandwidth(serviceCallback)
|
||||||
|
} catch (_: RemoteException) {
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var service: IShadowsocksService? = null
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||||
|
this.binder = binder
|
||||||
|
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||||
|
this.service = service
|
||||||
|
try {
|
||||||
|
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||||
|
check(!callbackRegistered)
|
||||||
|
service.registerCallback(serviceCallback)
|
||||||
|
callbackRegistered = true
|
||||||
|
if (bandwidthTimeout > 0) service.startListeningForBandwidth(
|
||||||
|
serviceCallback,
|
||||||
|
bandwidthTimeout
|
||||||
|
)
|
||||||
|
} catch (_: RemoteException) {
|
||||||
|
}
|
||||||
|
callback!!.onServiceConnected(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
unregisterCallback()
|
||||||
|
callback?.onServiceDisconnected()
|
||||||
|
service = null
|
||||||
|
binder = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun binderDied() {
|
||||||
|
service = null
|
||||||
|
callbackRegistered = false
|
||||||
|
callback?.also { handler.post(it::onBinderDied) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterCallback() {
|
||||||
|
val service = service
|
||||||
|
if (service != null && callbackRegistered) try {
|
||||||
|
service.unregisterCallback(serviceCallback)
|
||||||
|
} catch (_: RemoteException) {
|
||||||
|
}
|
||||||
|
callbackRegistered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connect(context: Context, callback: Callback) {
|
||||||
|
if (connectionActive) return
|
||||||
|
connectionActive = true
|
||||||
|
check(this.callback == null)
|
||||||
|
this.callback = callback
|
||||||
|
val intent = Intent(context, serviceClass).setAction(Action.SERVICE)
|
||||||
|
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect(context: Context) {
|
||||||
|
unregisterCallback()
|
||||||
|
if (connectionActive) try {
|
||||||
|
context.unbindService(this)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
} // ignore
|
||||||
|
connectionActive = false
|
||||||
|
if (listenForDeath) binder?.unlinkToDeath(this, 0)
|
||||||
|
binder = null
|
||||||
|
try {
|
||||||
|
service?.stopListeningForBandwidth(serviceCallback)
|
||||||
|
} catch (_: RemoteException) {
|
||||||
|
}
|
||||||
|
service = null
|
||||||
|
callback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.aidl
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
data class TrafficStats(
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
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> {
|
||||||
|
override fun createFromParcel(parcel: Parcel) = TrafficStats(parcel)
|
||||||
|
override fun newArray(size: Int): Array<TrafficStats?> = arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
348
client/android/src/com/github/shadowsocks/bg/BaseService.kt
Normal file
348
client/android/src/com/github/shadowsocks/bg/BaseService.kt
Normal file
|
|
@ -0,0 +1,348 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.Service
|
||||||
|
import android.content.Context
|
||||||
|
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 kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import java.util.*
|
||||||
|
import org.amnezia.vpn.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object uses WeakMap to simulate the effects of multi-inheritance.
|
||||||
|
*/
|
||||||
|
object BaseService {
|
||||||
|
enum class State(val canStop: Boolean = false) {
|
||||||
|
/**
|
||||||
|
* Idle state is only used by UI and will never be returned by BaseService.
|
||||||
|
*/
|
||||||
|
Idle,
|
||||||
|
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 notification: ServiceNotification? = null
|
||||||
|
val closeReceiver = broadcastReceiver { _, intent ->
|
||||||
|
when (intent.action) {
|
||||||
|
Intent.ACTION_SHUTDOWN -> service.persistStats()
|
||||||
|
Action.RELOAD -> service.forceLoad()
|
||||||
|
else -> service.stopRunner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var closeReceiverRegistered = false
|
||||||
|
|
||||||
|
val binder = Binder(this)
|
||||||
|
var connectingJob: Job? = null
|
||||||
|
|
||||||
|
fun changeState(s: State, msg: String? = null) {
|
||||||
|
if (state == s && msg == null) return
|
||||||
|
binder.stateChanged(s, msg)
|
||||||
|
state = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), CoroutineScope, AutoCloseable {
|
||||||
|
private 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
|
||||||
|
|
||||||
|
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||||
|
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
|
||||||
|
|
||||||
|
override fun registerCallback(cb: IShadowsocksServiceCallback) {
|
||||||
|
callbacks.register(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
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()) }
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var sum = TrafficStats()
|
||||||
|
val data = data
|
||||||
|
val proxy = data?.proxy ?: return@launch
|
||||||
|
proxy.trafficMonitor?.out.also { stats ->
|
||||||
|
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterCallback(cb: IShadowsocksServiceCallback) {
|
||||||
|
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
||||||
|
callbacks.unregister(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stateChanged(s: State, msg: String?) {
|
||||||
|
val profileName = profileName
|
||||||
|
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trafficPersisted(ids: List<Long>) {
|
||||||
|
if (bandwidthListeners.isNotEmpty() && ids.isNotEmpty()) broadcast { item ->
|
||||||
|
if (bandwidthListeners.contains(item.asBinder())) ids.forEach(item::trafficPersisted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
callbacks.kill()
|
||||||
|
cancel()
|
||||||
|
data = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Interface {
|
||||||
|
val data: Data
|
||||||
|
val tag: String
|
||||||
|
fun createNotification(profileName: String): ServiceNotification
|
||||||
|
|
||||||
|
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())) {
|
||||||
|
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val s = data.state
|
||||||
|
when {
|
||||||
|
s == State.Stopped -> startRunner()
|
||||||
|
s.canStop -> stopRunner(true)
|
||||||
|
// else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
data.proxy!!.start(this,
|
||||||
|
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||||
|
File(configRoot, CONFIG_FILE),
|
||||||
|
"-u")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startRunner() {
|
||||||
|
this as Context
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
|
||||||
|
else startService(Intent(this, javaClass))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun killProcesses(scope: CoroutineScope) {
|
||||||
|
data.processes?.run {
|
||||||
|
close(scope)
|
||||||
|
data.processes = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopRunner(restart: Boolean = false, msg: String? = null) {
|
||||||
|
if (data.state == State.Stopping) return
|
||||||
|
// 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
|
||||||
|
coroutineScope {
|
||||||
|
killProcesses(this)
|
||||||
|
// clean up receivers
|
||||||
|
val data = data
|
||||||
|
if (data.closeReceiverRegistered) {
|
||||||
|
unregisterReceiver(data.closeReceiver)
|
||||||
|
data.closeReceiverRegistered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data.notification?.destroy()
|
||||||
|
data.notification = null
|
||||||
|
|
||||||
|
val ids = listOfNotNull(data.proxy).map {
|
||||||
|
it.shutdown(this)
|
||||||
|
it.profile.id
|
||||||
|
}
|
||||||
|
data.proxy = null
|
||||||
|
data.binder.trafficPersisted(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the state
|
||||||
|
data.changeState(State.Stopped, msg)
|
||||||
|
|
||||||
|
// stop the service if nothing has bound to it
|
||||||
|
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 openConnection(url: URL) = url.openConnection()
|
||||||
|
|
||||||
|
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val data = data
|
||||||
|
if (data.state != State.Stopped) return Service.START_NOT_STICKY
|
||||||
|
val profilePair = Core.currentProfile
|
||||||
|
this as Context
|
||||||
|
if (profilePair == null) {
|
||||||
|
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
|
||||||
|
data.notification = createNotification("")
|
||||||
|
stopRunner(false, getString(R.string.profile_empty))
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
val (profile, _) = profilePair
|
||||||
|
profile.name = profile.formattedName // save name for later queries
|
||||||
|
val proxy = ProxyInstance(profile)
|
||||||
|
data.proxy = proxy
|
||||||
|
|
||||||
|
if (!data.closeReceiverRegistered) {
|
||||||
|
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||||
|
addAction(Action.RELOAD)
|
||||||
|
addAction(Intent.ACTION_SHUTDOWN)
|
||||||
|
addAction(Action.CLOSE)
|
||||||
|
})
|
||||||
|
data.closeReceiverRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
data.processes = GuardedProcessPool {
|
||||||
|
printLog(it)
|
||||||
|
stopRunner(false, it.readableMessage)
|
||||||
|
}
|
||||||
|
startProcesses(hosts)
|
||||||
|
// proxy.scheduleUpdate() // XinLake. Bypass-LAN only
|
||||||
|
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)
|
||||||
|
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
|
||||||
|
} finally {
|
||||||
|
data.connectingJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
client/android/src/com/github/shadowsocks/bg/Executable.kt
Normal file
56
client/android/src/com/github/shadowsocks/bg/Executable.kt
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.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 SS_LOCAL = "libss-local.so"
|
||||||
|
const val TUN2SOCKS = "libtun2socks.so"
|
||||||
|
|
||||||
|
private val EXECUTABLES = setOf(SS_LOCAL, TUN2SOCKS)
|
||||||
|
|
||||||
|
fun killAll() {
|
||||||
|
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
|
||||||
|
?: return) {
|
||||||
|
val exe = File(try {
|
||||||
|
File(process, "cmdline").inputStream().bufferedReader().readText()
|
||||||
|
} catch (_: IOException) {
|
||||||
|
continue
|
||||||
|
}.split(Character.MIN_VALUE, limit = 2).first())
|
||||||
|
if (EXECUTABLES.contains(exe.name)) try {
|
||||||
|
Os.kill(process.name.toInt(), OsConstants.SIGKILL)
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
if (e.errno != OsConstants.ESRCH) {
|
||||||
|
e.printStackTrace()
|
||||||
|
// Crashlytics.log(Log.WARN, "kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed")
|
||||||
|
// Crashlytics.logException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.os.Build
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.system.ErrnoException
|
||||||
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import com.github.shadowsocks.Core
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "GuardedProcessPool"
|
||||||
|
private val pid by lazy {
|
||||||
|
Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid").apply { isAccessible = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Guard(private val cmd: List<String>) {
|
||||||
|
private lateinit var process: Process
|
||||||
|
|
||||||
|
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
|
||||||
|
input.bufferedReader().forEachLine(logger)
|
||||||
|
} catch (_: IOException) {
|
||||||
|
} // ignore
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {
|
||||||
|
var running = true
|
||||||
|
val cmdName = File(cmd.first()).nameWithoutExtension
|
||||||
|
val exitChannel = Channel<Int>()
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
thread(name = "stderr-$cmdName") {
|
||||||
|
streamLogger(process.errorStream) {
|
||||||
|
// Crashlytics.log(Log.ERROR, cmdName, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread(name = "stdout-$cmdName") {
|
||||||
|
streamLogger(process.inputStream) {
|
||||||
|
// Crashlytics.log(Log.VERBOSE, 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"))
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
|
// clean-up cannot be cancelled
|
||||||
|
if (Build.VERSION.SDK_INT < 24) {
|
||||||
|
try {
|
||||||
|
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
if (e.errno != OsConstants.ESRCH) throw e
|
||||||
|
}
|
||||||
|
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext
|
||||||
|
}
|
||||||
|
process.destroy() // kill the process
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext
|
||||||
|
process.destroyForcibly() // Force to kill the process if it's still alive
|
||||||
|
}
|
||||||
|
exitChannel.receive()
|
||||||
|
} // otherwise process already exited, nothing to be done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
||||||
|
|
||||||
|
@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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun close(scope: CoroutineScope) {
|
||||||
|
cancel()
|
||||||
|
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 com.github.shadowsocks.net.HostsFile
|
||||||
|
import com.github.shadowsocks.net.LocalDnsServer
|
||||||
|
import com.github.shadowsocks.net.Socks5Endpoint
|
||||||
|
import com.github.shadowsocks.preference.DataStore
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object LocalDnsService {
|
||||||
|
private val googleApisTester =
|
||||||
|
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
|
||||||
|
|
||||||
|
private val servers = WeakHashMap<Interface, LocalDnsServer>()
|
||||||
|
|
||||||
|
interface Interface : BaseService.Interface {
|
||||||
|
override suspend fun startProcesses(hosts: HostsFile) {
|
||||||
|
super.startProcesses(hosts)
|
||||||
|
val profile = data.proxy!!.profile
|
||||||
|
val dns = try {
|
||||||
|
URI("dns://${profile.remoteDns}")
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
throw BaseService.ExpectedExceptionWrapper(e)
|
||||||
|
}
|
||||||
|
LocalDnsServer(this::resolver,
|
||||||
|
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||||
|
DataStore.proxyAddress,
|
||||||
|
hosts).apply {
|
||||||
|
tcp = !profile.udpdns
|
||||||
|
forwardOnly = true
|
||||||
|
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun killProcesses(scope: CoroutineScope) {
|
||||||
|
servers.remove(this)?.shutdown(scope)
|
||||||
|
super.killProcesses(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
108
client/android/src/com/github/shadowsocks/bg/TrafficMonitor.kt
Normal file
108
client/android/src/com/github/shadowsocks/bg/TrafficMonitor.kt
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.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 java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
class TrafficMonitor(statFile: File) {
|
||||||
|
val thread = object : LocalSocketListener("TrafficMonitor-" + statFile.name, statFile) {
|
||||||
|
private val buffer = ByteArray(16)
|
||||||
|
private val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
override fun acceptInternal(socket: LocalSocket) {
|
||||||
|
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
|
||||||
|
val tx = stat.getLong(0)
|
||||||
|
val rx = stat.getLong(8)
|
||||||
|
if (current.txTotal != tx) {
|
||||||
|
current.txTotal = tx
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
if (current.rxTotal != rx) {
|
||||||
|
current.rxTotal = rx
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply { start() }
|
||||||
|
|
||||||
|
val current = TrafficStats()
|
||||||
|
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()
|
||||||
|
val delta = now - timestampLast
|
||||||
|
timestampLast = now
|
||||||
|
var updated = false
|
||||||
|
if (delta != 0L) {
|
||||||
|
if (dirty) {
|
||||||
|
out = current.copy().apply {
|
||||||
|
txRate = (txTotal - out.txTotal) * 1000 / delta
|
||||||
|
rxRate = (rxTotal - out.rxTotal) * 1000 / delta
|
||||||
|
}
|
||||||
|
dirty = false
|
||||||
|
updated = true
|
||||||
|
} else {
|
||||||
|
if (out.txRate != 0L) {
|
||||||
|
out.txRate = 0
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
if (out.rxRate != 0L) {
|
||||||
|
out.rxRate = 0
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
client/android/src/com/github/shadowsocks/bg/VpnService.kt
Normal file
229
client/android/src/com/github/shadowsocks/bg/VpnService.kt
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.LocalSocket
|
||||||
|
import android.net.LocalSocketAddress
|
||||||
|
import android.net.Network
|
||||||
|
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.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 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 java.util.*
|
||||||
|
import android.net.VpnService as BaseVpnService
|
||||||
|
|
||||||
|
class VpnService : BaseVpnService(), 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<BaseVpnService>.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 androidx.room.*
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class KeyValuePair() {
|
||||||
|
companion object {
|
||||||
|
const val TYPE_UNINITIALIZED = 0
|
||||||
|
const val TYPE_BOOLEAN = 1
|
||||||
|
const val TYPE_FLOAT = 2
|
||||||
|
@Deprecated("Use TYPE_LONG.")
|
||||||
|
const val TYPE_INT = 3
|
||||||
|
const val TYPE_LONG = 4
|
||||||
|
const val TYPE_STRING = 5
|
||||||
|
const val TYPE_STRING_SET = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.room.Dao
|
||||||
|
interface Dao {
|
||||||
|
@Query("SELECT * FROM `KeyValuePair` WHERE `key` = :key")
|
||||||
|
operator fun get(key: String): KeyValuePair?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun put(value: KeyValuePair): Long
|
||||||
|
|
||||||
|
@Query("DELETE FROM `KeyValuePair` WHERE `key` = :key")
|
||||||
|
fun delete(key: String): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey
|
||||||
|
var key: String = ""
|
||||||
|
var valueType: Int = TYPE_UNINITIALIZED
|
||||||
|
var value: ByteArray = ByteArray(0)
|
||||||
|
|
||||||
|
val boolean: Boolean?
|
||||||
|
get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null
|
||||||
|
val float: Float?
|
||||||
|
get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@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 string: String?
|
||||||
|
get() = if (valueType == TYPE_STRING) String(value) else null
|
||||||
|
val stringSet: Set<String>?
|
||||||
|
get() = if (valueType == TYPE_STRING_SET) {
|
||||||
|
val buffer = ByteBuffer.wrap(value)
|
||||||
|
val result = HashSet<String>()
|
||||||
|
while (buffer.hasRemaining()) {
|
||||||
|
val chArr = ByteArray(buffer.int)
|
||||||
|
buffer.get(chArr)
|
||||||
|
result.add(String(chArr))
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(key: String) : this() {
|
||||||
|
this.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// putting null requires using DataStore
|
||||||
|
fun put(value: Boolean): KeyValuePair {
|
||||||
|
valueType = TYPE_BOOLEAN
|
||||||
|
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 {
|
||||||
|
valueType = TYPE_INT
|
||||||
|
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(v.toByteArray())
|
||||||
|
}
|
||||||
|
this.value = stream.toByteArray()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
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
|
||||||
|
|
||||||
|
@Database(entities = [Profile::class, KeyValuePair::class], version = 1000)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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`") {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
super.migrate(database)
|
||||||
|
PublicDatabase.Migration3.migrate(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
266
client/android/src/com/github/shadowsocks/database/Profile.kt
Normal file
266
client/android/src/com/github/shadowsocks/database/Profile.kt
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.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.json.JSONArray
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.sql.SQLException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLExceptions are not caught (and therefore will cause crash) for insert/update transactions
|
||||||
|
* to ensure we are in a consistent state.
|
||||||
|
*/
|
||||||
|
object ProfileManager {
|
||||||
|
interface Listener {
|
||||||
|
fun onAdd(profile: Profile)
|
||||||
|
fun onRemove(profileId: Long)
|
||||||
|
fun onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
@Throws(SQLException::class)
|
||||||
|
fun createProfile(profile: Profile = Profile()): Profile {
|
||||||
|
profile.id = 0
|
||||||
|
profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0
|
||||||
|
profile.id = PrivateDatabase.profileDao.create(profile)
|
||||||
|
listener?.onAdd(profile)
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createProfilesFromJson(jsons: Sequence<InputStream>, replace: Boolean = false) {
|
||||||
|
val profiles = if (replace) getAllProfiles()?.associateBy { it.formattedAddress } else null
|
||||||
|
val feature = if (replace) {
|
||||||
|
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) {
|
||||||
|
if (replace) {
|
||||||
|
lazyClear.value
|
||||||
|
// if two profiles has the same address, treat them as the same profile and copy stats over
|
||||||
|
profiles?.get(it.formattedAddress)?.apply {
|
||||||
|
it.tx = tx
|
||||||
|
it.rx = rx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createProfile(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) } }
|
||||||
|
return JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
|
||||||
|
*/
|
||||||
|
@Throws(SQLException::class)
|
||||||
|
fun updateProfile(profile: Profile) = check(PrivateDatabase.profileDao.update(profile) == 1)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getProfile(id: Long): Profile? = try {
|
||||||
|
PrivateDatabase.profileDao[id]
|
||||||
|
} catch (ex: SQLiteCantOpenDatabaseException) {
|
||||||
|
throw IOException(ex)
|
||||||
|
} catch (ex: SQLException) {
|
||||||
|
printLog(ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun expand(profile: Profile): Pair<Profile, Profile?> = Pair(profile, null)
|
||||||
|
|
||||||
|
@Throws(SQLException::class)
|
||||||
|
fun delProfile(id: Long) {
|
||||||
|
check(PrivateDatabase.profileDao.delete(id) == 1)
|
||||||
|
listener?.onRemove(id)
|
||||||
|
if (id in Core.activeProfileIds && DataStore.directBootAware) DirectBoot.clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SQLException::class)
|
||||||
|
fun clear() = PrivateDatabase.profileDao.deleteAll().also {
|
||||||
|
// listener is not called since this won't be used in mobile submodule
|
||||||
|
DirectBoot.clean()
|
||||||
|
listener?.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun ensureNotEmpty() {
|
||||||
|
val nonEmpty = try {
|
||||||
|
PrivateDatabase.profileDao.isNotEmpty()
|
||||||
|
} catch (ex: SQLiteCantOpenDatabaseException) {
|
||||||
|
throw IOException(ex)
|
||||||
|
} catch (ex: SQLException) {
|
||||||
|
printLog(ex)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (!nonEmpty) DataStore.profileId = createProfile().id
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getAllProfiles(): List<Profile>? = try {
|
||||||
|
PrivateDatabase.profileDao.list()
|
||||||
|
} catch (ex: SQLiteCantOpenDatabaseException) {
|
||||||
|
throw IOException(ex)
|
||||||
|
} catch (ex: SQLException) {
|
||||||
|
printLog(ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 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
|
||||||
|
|
||||||
|
@Database(entities = [KeyValuePair::class], version = 3)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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`")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2018 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.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) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("CREATE TABLE `tmp` $schema")
|
||||||
|
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")
|
||||||
|
database.execSQL("DROP TABLE `$table`")
|
||||||
|
database.execSQL("ALTER TABLE `tmp` RENAME TO `$table`")
|
||||||
|
}
|
||||||
|
}
|
||||||
129
client/android/src/com/github/shadowsocks/net/ChannelMonitor.kt
Normal file
129
client/android/src/com/github/shadowsocks/net/ChannelMonitor.kt
Normal file
|
|
@ -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 com.github.shadowsocks.net
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import com.github.shadowsocks.utils.printLog
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.*
|
||||||
|
|
||||||
|
class ChannelMonitor : Thread("ChannelMonitor") {
|
||||||
|
private data class Registration(val channel: SelectableChannel,
|
||||||
|
val ops: Int,
|
||||||
|
val listener: (SelectionKey) -> Unit) {
|
||||||
|
val result = CompletableDeferred<SelectionKey>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val selector = Selector.open()
|
||||||
|
private val registrationPipe = Pipe.open()
|
||||||
|
private val pendingRegistrations = Channel<Registration>(Channel.UNLIMITED)
|
||||||
|
private val closeChannel = Channel<Unit>(1)
|
||||||
|
@Volatile
|
||||||
|
private var running = true
|
||||||
|
|
||||||
|
private fun registerInternal(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit) =
|
||||||
|
channel.register(selector, ops, block)
|
||||||
|
|
||||||
|
init {
|
||||||
|
registrationPipe.source().apply {
|
||||||
|
configureBlocking(false)
|
||||||
|
registerInternal(this, SelectionKey.OP_READ) {
|
||||||
|
val junk = ByteBuffer.allocateDirect(1)
|
||||||
|
while (read(junk) > 0) {
|
||||||
|
pendingRegistrations.poll()!!.apply {
|
||||||
|
try {
|
||||||
|
result.complete(registerInternal(channel, ops, listener))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.completeExceptionally(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
junk.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
suspend fun register(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit): SelectionKey {
|
||||||
|
val registration = Registration(channel, ops, block)
|
||||||
|
pendingRegistrations.send(registration)
|
||||||
|
ByteBuffer.allocateDirect(1).also { junk ->
|
||||||
|
loop@ while (running) when (registrationPipe.sink().writeCompat(junk)) {
|
||||||
|
0 -> kotlinx.coroutines.yield()
|
||||||
|
1 -> break@loop
|
||||||
|
else -> throw IOException("Failed to register in the channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!running) throw CancellationException()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
while (running) {
|
||||||
|
val num = try {
|
||||||
|
selector.select()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
printLog(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (num <= 0) continue
|
||||||
|
val iterator = selector.selectedKeys().iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val key = iterator.next()
|
||||||
|
iterator.remove()
|
||||||
|
(key.attachment() as (SelectionKey) -> Unit)(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeChannel.sendBlocking(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close(scope: CoroutineScope) {
|
||||||
|
running = false
|
||||||
|
selector.wakeup()
|
||||||
|
scope.launch {
|
||||||
|
closeChannel.receive()
|
||||||
|
selector.keys().forEach { it.channel().close() }
|
||||||
|
selector.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
import android.net.LocalSocket
|
||||||
|
import com.github.shadowsocks.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) }
|
||||||
|
|
||||||
|
override fun accept(socket: LocalSocket) {
|
||||||
|
launch { super.accept(socket) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown(scope: CoroutineScope) {
|
||||||
|
running = false
|
||||||
|
cancel()
|
||||||
|
super.shutdown(scope)
|
||||||
|
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
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 kotlinx.coroutines.channels.actor
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
object DefaultNetworkListener {
|
||||||
|
private sealed class NetworkMessage {
|
||||||
|
class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
|
||||||
|
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
||||||
|
var network: Network? = null
|
||||||
|
val pendingRequests = arrayListOf<NetworkMessage.Get>()
|
||||||
|
for (message in channel) when (message) {
|
||||||
|
is NetworkMessage.Start -> {
|
||||||
|
if (listeners.isEmpty()) register()
|
||||||
|
listeners[message.key] = message.listener
|
||||||
|
if (network != null) message.listener(network)
|
||||||
|
}
|
||||||
|
is NetworkMessage.Get -> {
|
||||||
|
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
|
||||||
|
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()) {
|
||||||
|
network = null
|
||||||
|
unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkMessage.Put -> {
|
||||||
|
network = message.network
|
||||||
|
pendingRequests.forEach { it.response.complete(message.network) }
|
||||||
|
pendingRequests.clear()
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun start(key: Any, listener: (Network?) -> Unit) =
|
||||||
|
networkActor.send(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 {
|
||||||
|
networkActor.send(this)
|
||||||
|
response.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
|
||||||
|
|
||||||
|
// 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 onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
||||||
|
// // it's a good idea to refresh capabilities
|
||||||
|
// runBlocking { networkActor.send(NetworkMessage.Update(network)) }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onLost(network: Network) =
|
||||||
|
// runBlocking { networkActor.send(NetworkMessage.Lost(network)) }
|
||||||
|
// }
|
||||||
|
|
||||||
|
private var fallback = false
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||||
|
* satisfies default network capabilities but only THE default network. Unfortunately, we need to have
|
||||||
|
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregister() {}//= Core.connectivity.unregisterNetworkCallback(Callback)
|
||||||
|
}
|
||||||
40
client/android/src/com/github/shadowsocks/net/HostsFile.kt
Normal file
40
client/android/src/com/github/shadowsocks/net/HostsFile.kt
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
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>>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val configuredHostnames get() = map.size
|
||||||
|
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList()
|
||||||
|
}
|
||||||
121
client/android/src/com/github/shadowsocks/net/HttpsTest.kt
Normal file
121
client/android/src/com/github/shadowsocks/net/HttpsTest.kt
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2018 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.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.R
|
||||||
|
import com.github.shadowsocks.utils.useCancellable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
|
||||||
|
*/
|
||||||
|
class HttpsTest : ViewModel() {
|
||||||
|
sealed class Status {
|
||||||
|
protected abstract val status: CharSequence
|
||||||
|
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
|
||||||
|
private var shown = false
|
||||||
|
override fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) {
|
||||||
|
super.retrieve(setStatus, errorCallback)
|
||||||
|
if (shown) return
|
||||||
|
shown = true
|
||||||
|
errorCallback(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
conn.setRequestProperty("Connection", "close")
|
||||||
|
conn.instanceFollowRedirects = false
|
||||||
|
conn.useCaches = false
|
||||||
|
running = GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
status.value = conn.useCancellable {
|
||||||
|
try {
|
||||||
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
val code = responseCode
|
||||||
|
val elapsed = SystemClock.elapsedRealtime() - start
|
||||||
|
if (code == 204 || code == 200 && responseLength == 0L) Status.Success(elapsed)
|
||||||
|
else Status.Error.UnexpectedResponseCode(code)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Status.Error.IOFailure(e)
|
||||||
|
} finally {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelTest() {
|
||||||
|
running?.cancel()
|
||||||
|
running = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidate() {
|
||||||
|
cancelTest()
|
||||||
|
status.value = Status.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
private val URLConnection.responseLength: Long
|
||||||
|
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
|
||||||
|
}
|
||||||
194
client/android/src/com/github/shadowsocks/net/LocalDnsServer.kt
Normal file
194
client/android/src/com/github/shadowsocks/net/LocalDnsServer.kt
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
import com.github.shadowsocks.bg.BaseService
|
||||||
|
import com.github.shadowsocks.utils.printLog
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.xbill.DNS.*
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.*
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.DatagramChannel
|
||||||
|
import java.nio.channels.SelectionKey
|
||||||
|
import java.nio.channels.SocketChannel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple DNS conditional forwarder.
|
||||||
|
*
|
||||||
|
* No cache is provided as localResolver may change from time to time. We expect DNS clients to do cache themselves.
|
||||||
|
*
|
||||||
|
* Based on:
|
||||||
|
* https://github.com/bitcoinj/httpseed/blob/809dd7ad9280f4bc98a356c1ffb3d627bf6c7ec5/src/main/kotlin/dns.kt
|
||||||
|
* 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 {
|
||||||
|
/**
|
||||||
|
* Forward all requests to remote and ignore localResolver.
|
||||||
|
*/
|
||||||
|
var forwardOnly = false
|
||||||
|
/**
|
||||||
|
* Forward UDP queries to TCP.
|
||||||
|
*/
|
||||||
|
var tcp = true
|
||||||
|
var remoteDomainMatcher: Regex? = null
|
||||||
|
var localIpMatcher: List<Subnet> = emptyList()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LocalDnsServer"
|
||||||
|
private const val TIMEOUT = 10_000L
|
||||||
|
/**
|
||||||
|
* TTL returned from localResolver is set to 120. Android API does not provide TTL,
|
||||||
|
* so we suppose Android apps should not care about TTL either.
|
||||||
|
*/
|
||||||
|
private const val TTL = 120L
|
||||||
|
private const val UDP_PACKET_SIZE = 512
|
||||||
|
|
||||||
|
private fun prepareDnsResponse(request: Message) = Message(request.header.id).apply {
|
||||||
|
header.setFlag(Flags.QR.toInt()) // this is a response
|
||||||
|
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) }
|
||||||
|
|
||||||
|
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
|
||||||
|
configureBlocking(false)
|
||||||
|
try {
|
||||||
|
socket().bind(listen)
|
||||||
|
} catch (e: BindException) {
|
||||||
|
throw BaseService.ExpectedExceptionWrapper(e)
|
||||||
|
}
|
||||||
|
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePacket(channel: DatagramChannel) {
|
||||||
|
val buffer = ByteBuffer.allocateDirect(UDP_PACKET_SIZE)
|
||||||
|
val source = channel.receive(buffer)!!
|
||||||
|
buffer.flip()
|
||||||
|
launch {
|
||||||
|
val reply = resolve(buffer)
|
||||||
|
while (channel.send(reply, source) <= 0) monitor.wait(channel, SelectionKey.OP_WRITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolve(packet: ByteBuffer): ByteBuffer {
|
||||||
|
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)
|
||||||
|
return forward(packet)
|
||||||
|
}
|
||||||
|
return supervisorScope {
|
||||||
|
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
|
||||||
|
try {
|
||||||
|
if (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) }
|
||||||
|
} 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()
|
||||||
|
}
|
||||||
|
if (localResults.isEmpty()) return@supervisorScope remote.await()
|
||||||
|
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
|
||||||
|
remote.cancel()
|
||||||
|
cookDnsResponse(request, localResults.asIterable())
|
||||||
|
} 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)
|
||||||
|
else -> printLog(e)
|
||||||
|
}
|
||||||
|
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
||||||
|
header.rcode = Rcode.SERVFAIL
|
||||||
|
}.toWire())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
channel.configureBlocking(false)
|
||||||
|
channel.connect(proxy)
|
||||||
|
val wrapped = remoteDns.tcpWrap(packet)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else DatagramChannel.open().use { channel ->
|
||||||
|
channel.configureBlocking(false)
|
||||||
|
monitor.wait(channel, SelectionKey.OP_WRITE)
|
||||||
|
check(channel.send(remoteDns.udpWrap(packet), proxy) > 0)
|
||||||
|
val result = remoteDns.udpReceiveBuffer(UDP_PACKET_SIZE)
|
||||||
|
while (isActive) {
|
||||||
|
monitor.wait(channel, SelectionKey.OP_READ)
|
||||||
|
if (channel.receive(result) == proxy) break
|
||||||
|
result.clear()
|
||||||
|
}
|
||||||
|
result.flip()
|
||||||
|
remoteDns.udpUnwrap(result)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shutdown(scope: CoroutineScope) {
|
||||||
|
cancel()
|
||||||
|
monitor.close(scope)
|
||||||
|
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
import android.net.LocalServerSocket
|
||||||
|
import android.net.LocalSocket
|
||||||
|
import android.net.LocalSocketAddress
|
||||||
|
import android.system.ErrnoException
|
||||||
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import com.github.shadowsocks.utils.printLog
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name) {
|
||||||
|
private val localSocket = LocalSocket().apply {
|
||||||
|
socketFile.delete() // It's a must-have to close and reuse previous local socket.
|
||||||
|
bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||||
|
}
|
||||||
|
private val serverSocket = LocalServerSocket(localSocket.fileDescriptor)
|
||||||
|
private val closeChannel = Channel<Unit>(1)
|
||||||
|
@Volatile
|
||||||
|
protected var running = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
accept(serverSocket.accept())
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (running) printLog(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeChannel.sendBlocking(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun shutdown(scope: CoroutineScope) {
|
||||||
|
running = false
|
||||||
|
localSocket.fileDescriptor?.apply {
|
||||||
|
// see also: https://issuetracker.google.com/issues/36945762#comment15
|
||||||
|
if (valid()) try {
|
||||||
|
Os.shutdown(this, OsConstants.SHUT_RDWR)
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
// suppress fd inactive or already closed
|
||||||
|
if (e.errno != OsConstants.EBADF && e.errno != OsConstants.ENOTCONN) throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch { closeChannel.receive() }
|
||||||
|
}
|
||||||
|
}
|
||||||
132
client/android/src/com/github/shadowsocks/net/Socks5Endpoint.kt
Normal file
132
client/android/src/com/github/shadowsocks/net/Socks5Endpoint.kt
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
import com.github.shadowsocks.utils.parseNumericAddress
|
||||||
|
import net.sourceforge.jsocks.Socks4Message
|
||||||
|
import net.sourceforge.jsocks.Socks5Message
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
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 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")
|
||||||
|
}
|
||||||
|
ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply {
|
||||||
|
put(type.toByte())
|
||||||
|
if (numeric == null) put(bytes.size.toByte())
|
||||||
|
put(bytes)
|
||||||
|
putShort(port.toShort())
|
||||||
|
}
|
||||||
|
}.array()
|
||||||
|
private val headerReserved = max(3 + 3 + 16, 3 + dest.size)
|
||||||
|
|
||||||
|
fun tcpWrap(message: ByteBuffer): ByteBuffer {
|
||||||
|
check(message.remaining() < 65536) { "TCP message too large" }
|
||||||
|
return ByteBuffer.allocateDirect(8 + dest.size + message.remaining()).apply {
|
||||||
|
put(Socks5Message.SOCKS_VERSION.toByte())
|
||||||
|
put(1) // nmethods
|
||||||
|
put(0) // no authentication required
|
||||||
|
// header
|
||||||
|
put(Socks5Message.SOCKS_VERSION.toByte())
|
||||||
|
put(Socks4Message.REQUEST_CONNECT.toByte())
|
||||||
|
put(0) // reserved
|
||||||
|
put(dest)
|
||||||
|
// data
|
||||||
|
putShort(message.remaining().toShort())
|
||||||
|
put(message)
|
||||||
|
flip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
|
||||||
|
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]}")
|
||||||
|
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]}")
|
||||||
|
if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}")
|
||||||
|
val dataOffset = when (val type = 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")
|
||||||
|
} + 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()}")
|
||||||
|
buffer.mark()
|
||||||
|
buffer.position(buffer.limit()) // restore old position
|
||||||
|
buffer.limit(end)
|
||||||
|
readBytes(buffer.limit())
|
||||||
|
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 udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
|
||||||
|
fun udpUnwrap(packet: ByteBuffer) {
|
||||||
|
packet.tryPosition(3)
|
||||||
|
packet.tryPosition(6 + when (val type = 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")
|
||||||
|
})
|
||||||
|
packet.mark()
|
||||||
|
}
|
||||||
|
}
|
||||||
85
client/android/src/com/github/shadowsocks/net/Subnet.kt
Normal file
85
client/android/src/com/github/shadowsocks/net/Subnet.kt
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.net
|
||||||
|
|
||||||
|
import com.github.shadowsocks.utils.parseNumericAddress
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet> {
|
||||||
|
companion object {
|
||||||
|
fun fromString(value: String): Subnet? {
|
||||||
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
|
val parts = (value as java.lang.String).split("/", 2)
|
||||||
|
val addr = parts[0].parseNumericAddress() ?: return null
|
||||||
|
return if (parts.size == 2) try {
|
||||||
|
val prefixSize = parts[1].toInt()
|
||||||
|
if (prefixSize < 0 || prefixSize > addr.address.size shl 3) null else Subnet(addr, prefixSize)
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
null
|
||||||
|
} else Subnet(addr, addr.address.size shl 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val addressLength get() = address.address.size shl 3
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(prefixSize in 0..addressLength) { "prefixSize $prefixSize not in 0..$addressLength" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matches(other: InetAddress): Boolean {
|
||||||
|
if (address.javaClass != other.javaClass) return false
|
||||||
|
// TODO optimize?
|
||||||
|
val a = address.address
|
||||||
|
val b = other.address
|
||||||
|
var i = 0
|
||||||
|
while (i * 8 < prefixSize && i * 8 + 8 <= prefixSize) {
|
||||||
|
if (a[i] != b[i]) return false
|
||||||
|
++i
|
||||||
|
}
|
||||||
|
if (i * 8 == prefixSize) return true
|
||||||
|
val mask = 256 - (1 shl (i * 8 + 8 - prefixSize))
|
||||||
|
return (a[i].toInt() and mask) == (b[i].toInt() and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
|
||||||
|
|
||||||
|
private fun Byte.unsigned() = toInt() and 0xFF
|
||||||
|
override fun compareTo(other: Subnet): Int {
|
||||||
|
val addrThis = address.address
|
||||||
|
val addrThat = other.address.address
|
||||||
|
var result = addrThis.size.compareTo(addrThat.size) // IPv4 address goes first
|
||||||
|
if (result != 0) return result
|
||||||
|
for ((x, y) in addrThis zip addrThat) {
|
||||||
|
result = x.unsigned().compareTo(y.unsigned()) // undo sign extension of signed byte
|
||||||
|
if (result != 0) return result
|
||||||
|
}
|
||||||
|
return prefixSize.compareTo(other.prefixSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
val that = other as? Subnet
|
||||||
|
return address == that?.address && prefixSize == that.prefixSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int = Objects.hash(address, prefixSize)
|
||||||
|
}
|
||||||
68
client/android/src/com/github/shadowsocks/net/TcpFastOpen.kt
Normal file
68
client/android/src/com/github/shadowsocks/net/TcpFastOpen.kt
Normal file
|
|
@ -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 com.github.shadowsocks.net
|
||||||
|
|
||||||
|
import com.github.shadowsocks.utils.readableMessage
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
object TcpFastOpen {
|
||||||
|
private const val PATH = "/proc/sys/net/ipv4/tcp_fastopen"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is kernel version >= 3.7.1.
|
||||||
|
*/
|
||||||
|
val supported by lazy {
|
||||||
|
if (File(PATH).canRead()) return@lazy true
|
||||||
|
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()) {
|
||||||
|
in Int.MIN_VALUE..6 -> false
|
||||||
|
7 -> match.groupValues[3].toInt() >= 1
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.readableMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.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 java.net.InetSocketAddress
|
||||||
|
|
||||||
|
object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
|
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
|
||||||
|
// privateStore will only be used as temp storage for ProfileConfigFragment
|
||||||
|
val privateStore = RoomPreferenceDataStore(PrivateDatabase.kvPairDao)
|
||||||
|
|
||||||
|
init {
|
||||||
|
publicStore.registerChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
|
||||||
|
when (key) {
|
||||||
|
Key.id -> if (directBootAware) DirectBoot.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
publicStore.putString(key, value.toString())
|
||||||
|
value
|
||||||
|
} else parsePort(publicStore.getString(key), default + userIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileId: Long
|
||||||
|
get() = publicStore.getLong(Key.id) ?: 0
|
||||||
|
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"
|
||||||
|
var portProxy: Int
|
||||||
|
get() = getLocalPort(Key.portProxy, 1080)
|
||||||
|
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
||||||
|
val proxyAddress get() = InetSocketAddress("127.0.0.1", portProxy)
|
||||||
|
var portLocalDns: Int
|
||||||
|
get() = getLocalPort(Key.portLocalDns, 5450)
|
||||||
|
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize settings that have complicated default values.
|
||||||
|
*/
|
||||||
|
fun initGlobal() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var editingId: Long?
|
||||||
|
get() = privateStore.getLong(Key.id)
|
||||||
|
set(value) = privateStore.putLong(Key.id, value)
|
||||||
|
var dirty: Boolean
|
||||||
|
get() = privateStore.getBoolean(Key.dirty) ?: false
|
||||||
|
set(value) = privateStore.putBoolean(Key.dirty, value)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.preference
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
|
||||||
|
object EditTextPreferenceModifiers {
|
||||||
|
object Monospace : EditTextPreference.OnBindEditTextListener {
|
||||||
|
override fun onBindEditText(editText: EditText) {
|
||||||
|
editText.typeface = Typeface.MONOSPACE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.preference
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
|
interface OnPreferenceDataStoreChangeListener {
|
||||||
|
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.preference
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceDataStore
|
||||||
|
import com.github.shadowsocks.database.KeyValuePair
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||||
|
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()
|
||||||
|
fun getLong(key: String) = kvPairDao[key]?.long
|
||||||
|
fun getString(key: String) = kvPairDao[key]?.string
|
||||||
|
fun getStringSet(key: String) = kvPairDao[key]?.stringSet
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||||
|
override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue
|
||||||
|
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())
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: String) {
|
||||||
|
kvPairDao.delete(key)
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2018 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.utils
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import androidx.recyclerview.widget.SortedList
|
||||||
|
|
||||||
|
private sealed class ArrayIterator<out T> : Iterator<T> {
|
||||||
|
abstract val size: Int
|
||||||
|
abstract operator fun get(index: Int): T
|
||||||
|
private var count = 0
|
||||||
|
override fun hasNext() = count < size
|
||||||
|
override fun next(): T = if (hasNext()) this[count++] else throw NoSuchElementException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ClipDataIterator(private val data: ClipData) : ArrayIterator<ClipData.Item>() {
|
||||||
|
override val size get() = data.itemCount
|
||||||
|
override fun get(index: Int) = data.getItemAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ClipData.asIterable() = Iterable { ClipDataIterator(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) }
|
||||||
60
client/android/src/com/github/shadowsocks/utils/Constants.kt
Normal file
60
client/android/src/com/github/shadowsocks/utils/Constants.kt
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
object Key {
|
||||||
|
/**
|
||||||
|
* Public config that doesn't need to be kept secret.
|
||||||
|
*/
|
||||||
|
const val DB_PUBLIC = "config.db"
|
||||||
|
const val DB_PROFILE = "profile.db"
|
||||||
|
|
||||||
|
const val id = "profileId"
|
||||||
|
const val name = "profileName"
|
||||||
|
|
||||||
|
const val portProxy = "portProxy"
|
||||||
|
const val portLocalDns = "portLocalDns"
|
||||||
|
|
||||||
|
const val directBootAware = "directBootAware"
|
||||||
|
|
||||||
|
const val udpdns = "isUdpDns"
|
||||||
|
const val ipv6 = "isIpv6"
|
||||||
|
|
||||||
|
const val host = "proxy"
|
||||||
|
const val password = "sitekey"
|
||||||
|
const val method = "encMethod"
|
||||||
|
const val remotePort = "remotePortNum"
|
||||||
|
const val remoteDns = "remoteDns"
|
||||||
|
|
||||||
|
const val dirty = "profileDirty"
|
||||||
|
|
||||||
|
const val tfo = "tcp_fastopen"
|
||||||
|
const val hosts = "hosts"
|
||||||
|
const val assetUpdateTime = "assetUpdateTime"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@SuppressLint("Registered")
|
||||||
|
@TargetApi(24)
|
||||||
|
class DeviceStorageApp(context: Context) : Application() {
|
||||||
|
init {
|
||||||
|
attachBaseContext(context.createDeviceProtectedStorageContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thou shalt not get the REAL underlying application context which would no longer be operating under device
|
||||||
|
* protected storage.
|
||||||
|
*/
|
||||||
|
override fun getApplicationContext() = this
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.github.shadowsocks.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 java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
|
|
||||||
|
@TargetApi(24)
|
||||||
|
object DirectBoot : BroadcastReceiver() {
|
||||||
|
private val file = File(Core.deviceStorage.noBackupFilesDir, "directBootProfile")
|
||||||
|
private var registered = false
|
||||||
|
|
||||||
|
fun getDeviceProfile(): Pair<Profile, Profile?>? = try {
|
||||||
|
ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> }
|
||||||
|
} catch (_: IOException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clean() {
|
||||||
|
file.delete()
|
||||||
|
File(Core.deviceStorage.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
|
||||||
|
File(Core.deviceStorage.noBackupFilesDir, BaseService.CONFIG_FILE_UDP).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flushTrafficStats() {
|
||||||
|
getDeviceProfile()?.also { (profile, fallback) ->
|
||||||
|
if (profile.dirty) ProfileManager.updateProfile(profile)
|
||||||
|
if (fallback?.dirty == true) ProfileManager.updateProfile(fallback)
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listenForUnlock() {
|
||||||
|
if (registered) return
|
||||||
|
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
|
||||||
|
registered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
flushTrafficStats()
|
||||||
|
app.unregisterReceiver(this)
|
||||||
|
registered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
133
client/android/src/com/github/shadowsocks/utils/Utils.kt
Normal file
133
client/android/src/com/github/shadowsocks/utils/Utils.kt
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2018 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.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.system.Os
|
||||||
|
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") {
|
||||||
|
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
||||||
|
isAccessible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A slightly more performant variant of parseNumericAddress.
|
||||||
|
*
|
||||||
|
* Bug in Android 9.0 and lower: 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 <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 parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
||||||
|
val value = str?.toIntOrNull() ?: default
|
||||||
|
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 ContentResolver.openBitmap(uri: 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on: https://stackoverflow.com/a/26348729/2245107
|
||||||
|
*/
|
||||||
|
fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
if (!resolveAttribute(resId, typedValue, true)) throw Resources.NotFoundException()
|
||||||
|
return typedValue.resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
val Intent.datas
|
||||||
|
get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
|
||||||
|
|
||||||
|
fun printLog(t: Throwable) {
|
||||||
|
// Crashlytics.logException(t)
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Preference.remove() = parent!!.removePreference(this)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue