Merge branch 'dev' into secure-config-2
This commit is contained in:
commit
b36517babb
256 changed files with 82224 additions and 1420 deletions
303
.travis.yml
303
.travis.yml
|
|
@ -1,5 +1,3 @@
|
|||
language: cpp
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
|
@ -10,135 +8,328 @@ jobs:
|
|||
include:
|
||||
- name: MacOS
|
||||
os: osx
|
||||
osx_image: xcode12.5
|
||||
osx_image: xcode13.4
|
||||
language: cpp
|
||||
|
||||
env:
|
||||
- PATH=/usr/local/opt/ccache/libexec:$PATH
|
||||
- QT_VERSION=5.15.2
|
||||
- QIF_VERSION=4.1
|
||||
- QIF_VERSION=4.4
|
||||
- QT_BIN_DIR=$HOME/Qt/$QT_VERSION/clang_64/bin
|
||||
- QIF_BIN_DIR=$QT_BIN_DIR/../../../Tools/QtInstallerFramework/$QIF_VERSION/bin
|
||||
|
||||
install:
|
||||
- |
|
||||
if [ ! -f $QT_BIN_DIR/qmake ]; then \
|
||||
brew install p7zip ccache && \
|
||||
python3 -m pip install --upgrade pip && \
|
||||
pip install -U aqtinstall requests py7zr && \
|
||||
pip show aqtinstall && \
|
||||
aqt install-qt mac desktop $QT_VERSION clang_64 -m all -O $HOME/Qt && \
|
||||
aqt install-tool mac desktop tools_ifw -O $HOME/Qt ; \
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
if [ ! -f $HOME/Qt/$QT_VERSION/clang_64/bin/qmake ]; then \
|
||||
brew install p7zip && \
|
||||
python3 -m pip install --upgrade pip && \
|
||||
pip install -U aqtinstall requests py7zr && \
|
||||
pip show aqtinstall && \
|
||||
python3 -m aqt install --outputdir $HOME/Qt $QT_VERSION mac desktop clang_64 -m qtbase && \
|
||||
python3 -m aqt tool --outputdir $HOME/Qt mac tools_ifw $QIF_VERSION qt.tools.ifw.${QIF_VERSION/./};
|
||||
fi
|
||||
- bash deploy/build_macos.sh
|
||||
|
||||
after_script:
|
||||
- ccache --show-stats
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
token: $GH_TOKEN
|
||||
skip_cleanup: true
|
||||
cleanup: false
|
||||
file:
|
||||
- "AmneziaVPN_unsigned.dmg"
|
||||
- "AmneziaVPN.dmg"
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
cache:
|
||||
- ccache
|
||||
- directories:
|
||||
- $HOME/Qt
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
|
||||
# ------------------------------------------------------
|
||||
- name: Windows_x64
|
||||
os: windows
|
||||
language: cpp
|
||||
|
||||
env:
|
||||
- PATH=/c/Python39:/c/Python39/Scripts:$PATH
|
||||
- QT_VERSION=5.14.2
|
||||
- QIF_VERSION=4.1
|
||||
- QT_BIN_DIR="c:\\Qt\\$QT_VERSION\\msvc2017_64\\bin"
|
||||
- QT_VERSION=5.15.2
|
||||
- QIF_VERSION=4.4
|
||||
- QT_BIN_DIR="c:\\Qt\\$QT_VERSION\\msvc2019_64\\bin"
|
||||
- QIF_BIN_DIR="c:\\Qt\\Tools\\QtInstallerFramework\\${QIF_VERSION}\\bin"
|
||||
- BUILD_ARCH=64
|
||||
- MSVC_PATH_WIN="C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community"
|
||||
- MSVC_PATH="/C/Program Files (x86)/Microsoft Visual Studio/2019/Community"
|
||||
|
||||
before_install:
|
||||
- if [ ! -f /C/Qt/$QT_VERSION/msvc2017_64/bin/qmake ]; then choco install python --version 3.9.1; fi
|
||||
|
||||
script:
|
||||
- dir "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build"
|
||||
- dir "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\Common7\Tools"
|
||||
install:
|
||||
- if [ ! -f "$MSVC_PATH/VC/Auxiliary/Build/vcvars64.bat" ]; then choco install --ignorepackagecodes --no-progress -y visualstudio2019buildtools --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.ATLMFC --includeRecommended --nocache --installPath $MSVC_PATH_WIN"; fi
|
||||
- if [ ! -f /C/Qt/$QT_VERSION/msvc2019_64/bin/qmake ]; then choco install python --version 3.9.1; fi
|
||||
- |
|
||||
if [ ! -f /C/Qt/$QT_VERSION/msvc2017_64/bin/qmake ]; then \
|
||||
if [ ! -f /C/Qt/$QT_VERSION/msvc2019_64/bin/qmake ]; then \
|
||||
python -m pip install --upgrade pip && \
|
||||
pip3 install -U aqtinstall requests py7zr && \
|
||||
pip3 show aqtinstall && \
|
||||
python -m aqt install --outputdir /C/Qt $QT_VERSION windows desktop win64_msvc2017_64 -m qtbase && \
|
||||
python -m aqt tool --outputdir /C/Qt windows tools_ifw $QIF_VERSION qt.tools.ifw.${QIF_VERSION/./}; \
|
||||
aqt install-qt windows desktop $QT_VERSION win64_msvc2019_64 -m all -O /C/Qt && \
|
||||
aqt install-tool windows desktop tools_ifw -O /C/Qt ; \
|
||||
fi
|
||||
|
||||
script:
|
||||
- echo set BUILD_ARCH=$BUILD_ARCH > winbuild.bat
|
||||
- echo set QT_BIN_DIR="$QT_BIN_DIR" >> winbuild.bat
|
||||
- echo set QIF_BIN_DIR="$QIF_BIN_DIR" >> winbuild.bat
|
||||
- echo call \""C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools\\VC\\Auxiliary\\Build\\vcvars${BUILD_ARCH}.bat\"" >> winbuild.bat
|
||||
- echo call \""C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools\\Common7\\Tools\\VsDevCmd.bat\" -arch=amd64" >> winbuild.bat
|
||||
- echo set WIN_CERT_PW=$WIN_CERT_PW >> winbuild.bat
|
||||
- echo call \""%MSVC_PATH_WIN%\\VC\\Auxiliary\\Build\\vcvars${BUILD_ARCH}.bat\"" >> winbuild.bat
|
||||
- echo call \""%MSVC_PATH_WIN%\\Common7\\Tools\\VsDevCmd.bat\" -arch=amd64" >> winbuild.bat
|
||||
- echo call deploy\\build_windows.bat >> winbuild.bat
|
||||
- cmd //c winbuild.bat
|
||||
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
token: $GH_TOKEN
|
||||
skip_cleanup: true
|
||||
cleanup: false
|
||||
file:
|
||||
- "AmneziaVPN_x64.exe"
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- /C/Qt
|
||||
- $MSVC_PATH
|
||||
|
||||
# ------------------------------------------------------
|
||||
- name: Windows_x32
|
||||
os: windows
|
||||
language: cpp
|
||||
|
||||
env:
|
||||
- PATH=/c/Python39:/c/Python39/Scripts:$PATH
|
||||
- QT_VERSION=5.14.2
|
||||
- QIF_VERSION=4.1
|
||||
- QT_BIN_DIR="c:\\Qt\\${QT_VERSION}\\msvc2017\\bin"
|
||||
- QT_VERSION=5.15.2
|
||||
- QIF_VERSION=4.4
|
||||
- QT_BIN_DIR="c:\\Qt\\${QT_VERSION}\\msvc2019\\bin"
|
||||
- QIF_BIN_DIR="c:\\Qt\\Tools\\QtInstallerFramework\\${QIF_VERSION}\\bin"
|
||||
- BUILD_ARCH=32
|
||||
- MSVC_PATH_WIN="C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community"
|
||||
- MSVC_PATH="/C/Program Files (x86)/Microsoft Visual Studio/2019/Community"
|
||||
|
||||
before_install:
|
||||
- if [ ! -f /C/Qt/$QT_VERSION/msvc2017/bin/qmake ]; then choco install python --version 3.9.1; fi
|
||||
|
||||
script:
|
||||
- dir "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build"
|
||||
- dir "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\Common7\Tools"
|
||||
install:
|
||||
- if [ ! -f "$MSVC_PATH/VC/Auxiliary/Build/vcvars64.bat" ]; then choco install --ignorepackagecodes --no-progress -y visualstudio2019buildtools --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.ATLMFC --includeRecommended --nocache --installPath $MSVC_PATH_WIN"; fi
|
||||
- if [ ! -f /C/Qt/$QT_VERSION/msvc2019/bin/qmake ]; then choco install python --version 3.9.1; fi
|
||||
- |
|
||||
if [ ! -f /C/Qt/$QT_VERSION/msvc2017/bin/qmake ]; then \
|
||||
if [ ! -f /C/Qt/$QT_VERSION/msvc2019/bin/qmake ]; then \
|
||||
python -m pip install --upgrade pip && \
|
||||
pip3 install -U aqtinstall requests py7zr && \
|
||||
pip3 show aqtinstall && \
|
||||
python -m aqt install --outputdir /C/Qt $QT_VERSION windows desktop win32_msvc2017 -m qtbase && \
|
||||
python -m aqt tool --outputdir /C/Qt windows tools_ifw $QIF_VERSION qt.tools.ifw.${QIF_VERSION/./}; \
|
||||
aqt install-qt windows desktop $QT_VERSION win32_msvc2019 -m all -O /C/Qt && \
|
||||
aqt install-tool windows desktop tools_ifw -O /C/Qt ; \
|
||||
fi
|
||||
- echo call \""C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools\\Common7\\Tools\\VsDevCmd.bat\"" > winbuild.bat
|
||||
- echo set BUILD_ARCH=$BUILD_ARCH >> winbuild.bat
|
||||
|
||||
script:
|
||||
- echo set BUILD_ARCH=$BUILD_ARCH > winbuild.bat
|
||||
- echo set QT_BIN_DIR="$QT_BIN_DIR" >> winbuild.bat
|
||||
- echo set QIF_BIN_DIR="$QIF_BIN_DIR" >> winbuild.bat
|
||||
- echo call \""C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools\\VC\\Auxiliary\\Build\\vcvars${BUILD_ARCH}.bat\"" >> winbuild.bat
|
||||
- echo set WIN_CERT_PW=$WIN_CERT_PW >> winbuild.bat
|
||||
- echo call \""%MSVC_PATH_WIN%\\VC\\Auxiliary\\Build\\vcvars${BUILD_ARCH}.bat\"" >> winbuild.bat
|
||||
- echo call \""%MSVC_PATH_WIN%\\Common7\\Tools\\VsDevCmd.bat\"" >> winbuild.bat
|
||||
- echo call deploy\\build_windows.bat >> winbuild.bat
|
||||
- cmd //c winbuild.bat
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
token: $GH_TOKEN
|
||||
skip_cleanup: true
|
||||
cleanup: false
|
||||
file:
|
||||
- "AmneziaVPN_x32.exe"
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
cache:
|
||||
directories:
|
||||
- /C/Qt
|
||||
- $MSVC_PATH
|
||||
|
||||
# ------------------------------------------------------
|
||||
- name: Linux
|
||||
os: linux
|
||||
dist: focal
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- p7zip
|
||||
- python3
|
||||
- python3-pip
|
||||
- libgl-dev
|
||||
- mesa-common-dev
|
||||
- libpulse-dev
|
||||
|
||||
env:
|
||||
- QT_VERSION=5.15.2
|
||||
- QIF_VERSION=4.4
|
||||
- QT_BIN_DIR=$HOME/Qt/$QT_VERSION/gcc_64/bin
|
||||
- QIF_BIN_DIR=$QT_BIN_DIR/../../../Tools/QtInstallerFramework/$QIF_VERSION/bin
|
||||
|
||||
install:
|
||||
- |
|
||||
if [ ! -f $QT_BIN_DIR/qmake ]; then \
|
||||
python3 -m pip install --user $(whoami) --upgrade pip && \
|
||||
export PATH=$HOME/.local/bin:$PATH && \
|
||||
python3 -m pip install -U aqtinstall requests py7zr && \
|
||||
python3 -m pip show aqtinstall && \
|
||||
python3 -m aqt install-qt linux desktop $QT_VERSION gcc_64 -m all -O $HOME/Qt && \
|
||||
python3 -m aqt install-tool linux desktop tools_ifw -O $HOME/Qt ; \
|
||||
fi
|
||||
|
||||
script:
|
||||
- bash deploy/build_linux.sh
|
||||
|
||||
after_script:
|
||||
- ccache --show-stats
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
token: $GH_TOKEN
|
||||
cleanup: false
|
||||
file:
|
||||
- "AmneziaVPN.bundle"
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
cache:
|
||||
- ccache
|
||||
- directories:
|
||||
- $HOME/Qt
|
||||
|
||||
# ------------------------------------------------------
|
||||
- name: Android
|
||||
os: linux
|
||||
language: android
|
||||
dist: xenial
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- p7zip
|
||||
- python3
|
||||
- python3-pip
|
||||
|
||||
android:
|
||||
components:
|
||||
# Uncomment the lines below if you want to
|
||||
# use the latest revision of Android SDK Tools
|
||||
# - tools
|
||||
# - platform-tools
|
||||
|
||||
# The BuildTools version used by your project
|
||||
- build-tools-30.0.2
|
||||
|
||||
# The SDK version used to compile your project
|
||||
- android-30
|
||||
|
||||
# Additional components
|
||||
- extra
|
||||
- extra-google-google_play_services
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
|
||||
env:
|
||||
- QT_VERSION=5.15.2
|
||||
- QT_BIN_DIR=$HOME/Qt/$QT_VERSION/android/bin
|
||||
- USE_ANDROID_NDK_VERSION=21d
|
||||
- ANDROID_NDK_HOME=$HOME/NDK
|
||||
|
||||
install:
|
||||
- |
|
||||
if [ ! -f $QT_BIN_DIR/qmake ]; then \
|
||||
export PATH=$HOME/.local/bin:$PATH && \
|
||||
python3 -m pip install -U aqtinstall requests py7zr && \
|
||||
python3 -m pip show aqtinstall && \
|
||||
python3 -m aqt install-qt linux android $QT_VERSION android_armv7 -m all -O $HOME/Qt && \
|
||||
python3 -m aqt install-qt linux android $QT_VERSION android_arm64_v8a -m all -O $HOME/Qt && \
|
||||
python3 -m aqt install-qt linux android $QT_VERSION android_x86_64 -m all -O $HOME/Qt && \
|
||||
python3 -m aqt install-qt linux android $QT_VERSION android_x86 -m all -O $HOME/Qt ; \
|
||||
fi
|
||||
- |
|
||||
export TERM=dumb &&
|
||||
curl -L https://dl.google.com/android/repository/android-ndk-r${USE_ANDROID_NDK_VERSION}-linux-x86_64.zip -O &&
|
||||
unzip ./android-ndk-r${USE_ANDROID_NDK_VERSION}-linux-x86_64.zip > /dev/null &&
|
||||
rm android-ndk-r${USE_ANDROID_NDK_VERSION}-linux-x86_64.zip &&
|
||||
export ANDROID_NDK_HOME=`pwd`/android-ndk-r${USE_ANDROID_NDK_VERSION} &&
|
||||
export LOCAL_ANDROID_NDK_HOME="$ANDROID_NDK_HOME" &&
|
||||
export LOCAL_ANDROID_NDK_HOST_PLATFORM="linux-x86_64" &&
|
||||
export PATH=$PATH:${ANDROID_NDK_HOME} &&
|
||||
env
|
||||
|
||||
script:
|
||||
- bash deploy/build_android.sh
|
||||
|
||||
after_script:
|
||||
- ccache --show-stats
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
token: $GH_TOKEN
|
||||
cleanup: false
|
||||
file:
|
||||
- "AmneziaVPN.aab"
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
cache:
|
||||
- ccache
|
||||
- directories:
|
||||
- $HOME/Qt
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
- $HOME/.android/build-cache
|
||||
- $ANDROID_NDK_HOME
|
||||
|
||||
# ------------------------------------------------------
|
||||
- name: iOS
|
||||
os: osx
|
||||
osx_image: xcode13.4
|
||||
language: cpp
|
||||
|
||||
env:
|
||||
- PATH=/usr/local/opt/ccache/libexec:~/go/bin:$PATH
|
||||
- QT_VERSION=5.15.2
|
||||
- QT_BIN_DIR=$HOME/Qt/$QT_VERSION/ios/bin
|
||||
- QT_IOS_BIN=$QT_BIN_DIR
|
||||
|
||||
install:
|
||||
- |
|
||||
if [ ! -f $QT_BIN_DIR/qmake ]; then \
|
||||
brew install p7zip ccache && \
|
||||
python3 -m pip install --upgrade pip && \
|
||||
pip install -U aqtinstall requests py7zr && \
|
||||
pip show aqtinstall && \
|
||||
aqt install-qt mac ios $QT_VERSION -m all -O $HOME/Qt ; \
|
||||
fi
|
||||
- brew install golang
|
||||
- go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
- gomobile init
|
||||
|
||||
script:
|
||||
- bash deploy/build_ios.sh
|
||||
|
||||
after_script:
|
||||
- ccache --show-stats
|
||||
|
||||
cache:
|
||||
- ccache
|
||||
- directories:
|
||||
- $HOME/Qt
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
|
||||
before_cache:
|
||||
- if [ "${TRAVIS_OS_NAME}" = "osx" ]; then brew cleanup; fi
|
||||
# Cache only .git files under "/usr/local/Homebrew" so "brew update" does not take 5min every build
|
||||
- if [ "${TRAVIS_OS_NAME}" = "osx" ]; then find /usr/local/Homebrew \! -regex ".+\.git.+" -delete; fi
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/Qt
|
||||
- /C/Qt
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ AmneziaVPN uses a number of open source projects to work:
|
|||
- [OpenVPN](https://openvpn.net/)
|
||||
- [ShadowSocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [EasyRSA](https://github.com/OpenVPN/easy-rsa) - part of OpenVPN
|
||||
- [CygWin](https://www.cygwin.com/) - only for Windiws, used for launching EasyRSA scripts
|
||||
- [QtSsh](https://github.com/jaredtao/QtSsh) - forked form Qt Creator
|
||||
- and more...
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit b906ecb614d93a69ef78c67bfd240554fbc95270
|
||||
Subproject commit 71799a8f6d1450b63071a21cad6ed434b348d3d5
|
||||
|
|
@ -17,8 +17,9 @@
|
|||
<!-- %%INSERT_FEATURES -->
|
||||
|
||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/splashScreenTheme">
|
||||
|
||||
<application android:name=".qt.AmneziaApp" android:hardwareAccelerated="true" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name=".qt.VPNActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/splashScreenTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
@ -78,17 +79,24 @@
|
|||
<!-- extract android style -->
|
||||
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
|
||||
</activity>
|
||||
|
||||
<service android:name=".VPNService"
|
||||
android:process=":QtOnlyProcess"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<service android:name=".VPNService" android:permission="android.permission.BIND_VPN_SERVICE" android:process=":QtOnlyProcess">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
|
||||
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
|
||||
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
|
||||
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
|
||||
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
|
||||
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
|
||||
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
</service>
|
||||
|
||||
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper" android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
</service>
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</application>
|
||||
|
|
|
|||
10377
client/android/assets/acl/bypass-china.acl
Normal file
10377
client/android/assets/acl/bypass-china.acl
Normal file
File diff suppressed because it is too large
Load diff
10391
client/android/assets/acl/bypass-lan-china.acl
Normal file
10391
client/android/assets/acl/bypass-lan-china.acl
Normal file
File diff suppressed because it is too large
Load diff
17
client/android/assets/acl/bypass-lan.acl
Normal file
17
client/android/assets/acl/bypass-lan.acl
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[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/29
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.18.0.0/15
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/3
|
||||
5245
client/android/assets/acl/china-list.acl
Normal file
5245
client/android/assets/acl/china-list.acl
Normal file
File diff suppressed because it is too large
Load diff
5492
client/android/assets/acl/gfwlist.acl
Normal file
5492
client/android/assets/acl/gfwlist.acl
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,5 @@
|
|||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
buildscript {
|
||||
ext{
|
||||
kotlin_version = "1.4.30-M1"
|
||||
|
|
@ -19,6 +21,8 @@ buildscript {
|
|||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
||||
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
}
|
||||
|
|
@ -34,15 +38,22 @@ apply plugin: 'com.android.application'
|
|||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02"
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
||||
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||
implementation project(path: ':shadowsocks')
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -77,6 +88,7 @@ android {
|
|||
renderscript.srcDirs = ['src']
|
||||
assets.srcDirs = ['assets']
|
||||
jniLibs.srcDirs = ['libs']
|
||||
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,9 +97,13 @@ android {
|
|||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
|
|
@ -104,6 +120,10 @@ android {
|
|||
targetSdkVersion = 30
|
||||
versionCode 10 // Change to a higher number
|
||||
versionName "2.0.10" // Change to a higher number
|
||||
|
||||
javaCompileOptions.annotationProcessorOptions.arguments = [
|
||||
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString()
|
||||
]
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# Gradle caching allows reusing the build artifacts from a previous
|
||||
# build with the same inputs. However, over time, the cache size will
|
||||
|
|
@ -21,3 +21,7 @@ androidBuildToolsVersion=30.0.2
|
|||
androidCompileSdkVersion=30
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
android.enableJetifier=true
|
||||
android.injected.testOnly=false
|
||||
kapt.use.worker.api=false
|
||||
kapt.incremental.apt=false
|
||||
|
|
|
|||
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>
|
||||
5215
client/android/res/raw/china_ip_list.txt
Normal file
5215
client/android/res/raw/china_ip_list.txt
Normal file
File diff suppressed because it is too large
Load diff
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>
|
||||
169
client/android/res/values/strings.xml
Normal file
169
client/android/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">shadowsocks</string>
|
||||
|
||||
<string name="service_mode_vpn">VPN</string>
|
||||
<string name="speed">%s/s</string>
|
||||
|
||||
|
||||
<string name="quick_toggle">"Switch"</string>
|
||||
<string name="remote_dns">"Remote DNS"</string>
|
||||
<string name="stat_summary">"Upload: \t%3$s\t↑\t%1$s
|
||||
Download: \t%4$s\t↓\t%2$s"</string>
|
||||
<string name="connection_test_testing">"Testing…"</string>
|
||||
<string name="connection_test_available">"Connection successful: HTTPS handshake delay %d milliseconds"</string>
|
||||
<string name="connection_test_error">"Failed: %s"</string>
|
||||
<string name="connection_test_fail">"No Internet Connection"</string>
|
||||
<string name="connection_test_error_status_code">"Invalid status code (#%d) "</string>
|
||||
|
||||
<!-- proxy category -->
|
||||
<string name="profile_name">"Profile name"</string>
|
||||
<string name="proxy">"Server"</string>
|
||||
<string name="remote_port">"Remote Port"</string>
|
||||
<string name="sitekey">"Password"</string>
|
||||
<string name="enc_method">"Encryption"</string>
|
||||
|
||||
<!-- feature category -->
|
||||
<string name="ipv6">"IPv6 routing"</string>
|
||||
<string name="ipv6_summary">"Forward IPv6 traffic to remote server"</string>
|
||||
<string name="route_list">"Routing"</string>
|
||||
<string name="route_entry_gfwlist">"GFW List"</string>
|
||||
<string name="proxied_apps">"Proxied VPN"</string>
|
||||
<string name="proxied_apps_summary">"Allow some apps to bypass VPN"</string>
|
||||
<string name="on">"On"</string>
|
||||
<string name="bypass_apps">"Bypass"</string>
|
||||
<string name="bypass_apps_summary">"Bypass selected apps"</string>
|
||||
<string name="auto_connect">"Auto connect"</string>
|
||||
<string name="auto_connect_summary">"Allow Shadowsocks to start with the system"</string>
|
||||
<string name="tcp_fastopen_summary">"Switching may require ROOT permissions"</string>
|
||||
<string name="tcp_fastopen_summary_unsupported">"Unsupported kernel version: %s < 3.7.1"</string>
|
||||
<string name="udp_dns">"Using UDP DNS"</string>
|
||||
<string name="udp_dns_summary">"Requires remote server to support UDP forwarding"</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">"Background service has started running. "</string>
|
||||
<string name="invalid_server">"Invalid server name"</string>
|
||||
<string name="service_failed">"Unable to connect to remote server"</string>
|
||||
<string name="stop">"Stop"</string>
|
||||
<string name="stopping">"stopping…"</string>
|
||||
<string name="vpn_error">"Background service failed to start: %s"</string>
|
||||
<string name="reboot_required">"VPN service failed to start. You may need to restart your device."</string>
|
||||
<string name="profile_invalid_input">"No valid configuration file found."</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="profile_empty">"Please select a profile"</string>
|
||||
<string name="proxy_empty">"The proxy server address and password cannot be empty"</string>
|
||||
<string name="connect">"Connect"</string>
|
||||
|
||||
<!-- menu category -->
|
||||
<string name="profiles">"Profiles"</string>
|
||||
<string name="settings">"Settings"</string>
|
||||
<string name="faq">"FAQ"</string>
|
||||
<string name="about">"About"</string>
|
||||
<string name="about_title">"Shadowsocks %s"</string>
|
||||
<string name="edit">"Edit"</string>
|
||||
<string name="share">"Share"</string>
|
||||
<string name="add_profile">"Add Profile"</string>
|
||||
<string name="action_apply_all">"Apply settings to all profiles"</string>
|
||||
<string name="action_export">"Export to clipboard"</string>
|
||||
<string name="action_import">"Import from clipboard"</string>
|
||||
<string name="action_export_msg">"Export to clipboard succeeded"</string>
|
||||
<string name="action_export_err">"Export to clipboard failed"</string>
|
||||
<string name="action_import_msg">"Import successful"</string>
|
||||
<string name="action_import_err">"Import failed"</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_config">"Profile Config"</string>
|
||||
<string name="delete">"Delete"</string>
|
||||
<string name="delete_confirm_prompt">"Are you sure you want to delete this profile?"</string>
|
||||
<string name="share_qr_nfc">"QR code / NFC"</string>
|
||||
<string name="add_profile_dialog">"Add this profile for Shadowsock?"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"Scan QR code"</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"%d items deleted"</item>
|
||||
</plurals>
|
||||
<string name="undo">"Undo"</string>
|
||||
|
||||
<!-- tasker -->
|
||||
<string name="toggle_service_state">"Start service"</string>
|
||||
<string name="start_service_default">"Connect to the current server"</string>
|
||||
<string name="start_service">"Connect to %s"</string>
|
||||
<string name="stop_service">"Switch to %s"</string>
|
||||
<string name="profile_default">"Use current profile"</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="sent">"Send: "</string>
|
||||
<string name="received">"Received:"</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="connecting">"connecting…"</string>
|
||||
<string name="vpn_connected">"Connected, click Test Connection"</string>
|
||||
<string name="not_connected">"Not connected"</string>
|
||||
|
||||
<!-- acl -->
|
||||
<string name="custom_rules">"Custom rules"</string>
|
||||
<string name="action_add_rule">"Add rule…"</string>
|
||||
<string name="edit_rule">"Edit rules"</string>
|
||||
<string name="route_entry_all">"Global"</string>
|
||||
<string name="route_entry_bypass_lan">"Bypass LAN addresses"</string>
|
||||
<string name="route_entry_bypass_chn">"Bypass mainland China addresses"</string>
|
||||
<string name="route_entry_bypass_lan_chn">"Bypass LAN and Mainland China addresses"</string>
|
||||
<string name="route_entry_chinalist">"Proxy only for mainland China addresses"</string>
|
||||
<string name="acl_rule_templates_generic">"Subnet/Domain PCRE Regular Expression"</string>
|
||||
<string name="acl_rule_templates_domain">"Domain names and their subdomains"</string>
|
||||
|
||||
<!-- plugin -->
|
||||
<string name="plugin">"Plugin"</string>
|
||||
<string name="plugin_configure">"Configure…"</string>
|
||||
<string name="plugin_disabled">"Disabled"</string>
|
||||
<string name="plugin_unknown">"Unknown plugin %s"</string>
|
||||
<string name="plugin_untrusted">"Warning: This plugin does not appear to be from a known trusted source."</string>
|
||||
<string name="profile_plugin">"Plugin: %s"</string>
|
||||
<string name="add_profile_scanner_permission_required">"Scanning the QR code requires permission to use the camera."</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="service_vpn">"VPN service"</string>
|
||||
<string name="add_profile_methods_manual_settings">"Manual setting"</string>
|
||||
|
||||
<!-- misc -->
|
||||
<string name="advanced">"Advanced options"</string>
|
||||
|
||||
<!-- misc -->
|
||||
<string name="service_mode">"Service mode"</string>
|
||||
<string name="service_mode_proxy">"Proxy only"</string>
|
||||
<string name="service_mode_transproxy">"Transparent proxy"</string>
|
||||
<string name="port_proxy">"SOCKS5 proxy port"</string>
|
||||
<string name="port_local_dns">"local DNS port"</string>
|
||||
<string name="port_transproxy">"Transparent proxy port"</string>
|
||||
<string name="service_proxy">"Proxy mode"</string>
|
||||
<string name="service_transproxy">"Transparent proxy mode"</string>
|
||||
<string name="vpn_permission_denied">"Insufficient permission to create VPN service"</string>
|
||||
<string name="auto_connect_summary_v24">"Allow Shadowsocks to start with the system, an always-on VPN is recommended"</string>
|
||||
<string name="direct_boot_aware">"Allow toggle on lock screen"</string>
|
||||
<string name="direct_boot_aware_summary">"The selected configuration information will be less secure"</string>
|
||||
<string name="acl_rule_online_config">"Online Rules File URL"</string>
|
||||
<string name="action_import_file">"Import from file…"</string>
|
||||
<string name="night_mode">"Night Mode"</string>
|
||||
<string name="night_mode_system">"System"</string>
|
||||
<string name="night_mode_auto">"Auto"</string>
|
||||
<string name="night_mode_on">"On"</string>
|
||||
<string name="night_mode_off">"Off"</string>
|
||||
<string name="send_email">"Send email"</string>
|
||||
<string name="action_export_more">"Export…"</string>
|
||||
<string name="action_export_file">"Export to file…"</string>
|
||||
<string name="cleartext_http_warning">"HTTP clear text traffic is not secure"</string>
|
||||
<string name="share_over_lan">"Share via LAN"</string>
|
||||
<string name="connection_test_pending">"Check connection"</string>
|
||||
<string name="file_manager_missing">"Please install a file manager such as MiXplorer"</string>
|
||||
<string name="tcp_fastopen_failure">"Failed to switch"</string>
|
||||
<string name="udp_fallback">"UDP configuration"</string>
|
||||
<string name="action_replace_file">"Replace from file…"</string>
|
||||
<string name="off">"Off"</string>
|
||||
<string name="proxied_apps_mode">"model"</string>
|
||||
<string name="proxy_cat">"Server settings"</string>
|
||||
<string name="feature_cat">"Function settings"</string>
|
||||
<string name="unsaved_changes_prompt">"Do you want to save the changes?"</string>
|
||||
<string name="yes">"Yes"</string>
|
||||
<string name="no">"No"</string>
|
||||
<string name="apply">"Apply"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1000,
|
||||
"identityHash": "14b379f7776710b79b9d617090efe40e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Profile",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remotePort",
|
||||
"columnName": "remotePort",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "method",
|
||||
"columnName": "method",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteDns",
|
||||
"columnName": "remoteDns",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "udpdns",
|
||||
"columnName": "udpdns",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv6",
|
||||
"columnName": "ipv6",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tx",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rx",
|
||||
"columnName": "rx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userOrder",
|
||||
"columnName": "userOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "KeyValuePair",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "valueType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14b379f7776710b79b9d617090efe40e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "f1aab1fb633378621635c344dbc8ac7b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "KeyValuePair",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "valueType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f1aab1fb633378621635c344dbc8ac7b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
1
client/android/settings.gradle
Normal file
1
client/android/settings.gradle
Normal file
|
|
@ -0,0 +1 @@
|
|||
include ':shadowsocks'
|
||||
82
client/android/shadowsocks/build.gradle
Normal file
82
client/android/shadowsocks/build.gradle
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
//apply plugin: 'com.novoda.bintray-release'
|
||||
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
//def lifecycleVersion = '2.0.0'
|
||||
//def roomVersion = '2.0.0'
|
||||
//def preferencexVersion = '1.0.0'
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30-M1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
|
||||
|
||||
implementation "androidx.core:core-ktx:1.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||
implementation "androidx.room:room-runtime:2.2.5" // runtime
|
||||
implementation "androidx.preference:preference:1.1.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.3.4"
|
||||
implementation "androidx.browser:browser:1.3.0-alpha01"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation "com.google.android.material:material:1.2.0-alpha05"
|
||||
implementation "com.google.code.gson:gson:2.8.5"
|
||||
|
||||
implementation "dnsjava:dnsjava:2.1.9"
|
||||
implementation "org.connectbot.jsocks:jsocks:1.0.0"
|
||||
implementation "com.afollestad.material-dialogs:core:2.6.0"
|
||||
// api "com.takisoft.preferencex:preferencex:1.0.0"
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||
api 'org.connectbot.jsocks:jsocks:1.0.0'
|
||||
kapt "androidx.room:room-compiler:2.2.5"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:2.4.0"
|
||||
}
|
||||
12
client/android/shadowsocks/gfwlist/gen.pl
Normal file
12
client/android/shadowsocks/gfwlist/gen.pl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env perl
|
||||
## ArchLinux install package via pacman: perl-net-cidr-lite
|
||||
use strict;
|
||||
use warnings;
|
||||
use Net::CIDR::Lite;
|
||||
my $cidr = Net::CIDR::Lite->new;
|
||||
while (my $line=<>) {
|
||||
$cidr->add($line);
|
||||
}
|
||||
foreach my $line( @{$cidr->list} ) {
|
||||
print "<item>$line</item>\n";
|
||||
}
|
||||
20
client/android/shadowsocks/gfwlist/gen.py
Normal file
20
client/android/shadowsocks/gfwlist/gen.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/python
|
||||
# -*- encoding: utf8 -*-
|
||||
|
||||
import sys
|
||||
|
||||
import IPy
|
||||
|
||||
|
||||
def main():
|
||||
china_list_set = IPy.IPSet()
|
||||
for line in sys.stdin:
|
||||
china_list_set.add(IPy.IP(line))
|
||||
|
||||
# 输出结果
|
||||
for ip in china_list_set:
|
||||
print '<item>' + str(ip) + '</item>'
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
121
client/android/shadowsocks/gfwlist/parse.py
Normal file
121
client/android/shadowsocks/gfwlist/parse.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pkgutil
|
||||
import urlparse
|
||||
import socket
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from datetime import date
|
||||
|
||||
__all__ = ['main']
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('-i', '--input', dest='input', required=True,
|
||||
help='path to gfwlist', metavar='GFWLIST')
|
||||
parser.add_argument('-f', '--file', dest='output', required=True,
|
||||
help='path to output acl', metavar='ACL')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def decode_gfwlist(content):
|
||||
# decode base64 if have to
|
||||
try:
|
||||
return content.decode('base64')
|
||||
except:
|
||||
return content
|
||||
|
||||
|
||||
def get_hostname(something):
|
||||
try:
|
||||
# quite enough for GFW
|
||||
if not something.startswith('http:'):
|
||||
something = 'http://' + something
|
||||
r = urlparse.urlparse(something)
|
||||
return r.hostname
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
|
||||
|
||||
def add_domain_to_set(s, something):
|
||||
hostname = get_hostname(something)
|
||||
if hostname is not None:
|
||||
if hostname.startswith('.'):
|
||||
hostname = hostname.lstrip('.')
|
||||
if hostname.endswith('/'):
|
||||
hostname = hostname.rstrip('/')
|
||||
if hostname:
|
||||
s.add(hostname)
|
||||
|
||||
|
||||
def parse_gfwlist(content):
|
||||
gfwlist = content.splitlines(False)
|
||||
domains = set()
|
||||
for line in gfwlist:
|
||||
if line.find('.*') >= 0:
|
||||
continue
|
||||
elif line.find('*') >= 0:
|
||||
line = line.replace('*', '/')
|
||||
if line.startswith('!'):
|
||||
continue
|
||||
elif line.startswith('['):
|
||||
continue
|
||||
elif line.startswith('@'):
|
||||
# ignore white list
|
||||
continue
|
||||
elif line.startswith('||'):
|
||||
add_domain_to_set(domains, line.lstrip('||'))
|
||||
elif line.startswith('|'):
|
||||
add_domain_to_set(domains, line.lstrip('|'))
|
||||
elif line.startswith('.'):
|
||||
add_domain_to_set(domains, line.lstrip('.'))
|
||||
else:
|
||||
add_domain_to_set(domains, line)
|
||||
# TODO: reduce ['www.google.com', 'google.com'] to ['google.com']
|
||||
return domains
|
||||
|
||||
|
||||
def generate_acl(domains):
|
||||
header ="""#
|
||||
# GFW list from https://github.com/gfwlist/gfwlist/blob/master/gfwlist.txt
|
||||
# updated on DATE
|
||||
#
|
||||
|
||||
[bypass_all]
|
||||
|
||||
[proxy_list]
|
||||
|
||||
"""
|
||||
header = header.replace('DATE', str(date.today()))
|
||||
proxy_content = ""
|
||||
ip_content = ""
|
||||
|
||||
for domain in sorted(domains):
|
||||
try:
|
||||
socket.inet_aton(domain)
|
||||
ip_content += (domain + "\n")
|
||||
except socket.error:
|
||||
domain = domain.replace('.', '\.')
|
||||
proxy_content += ('(^|\.)' + domain + '$\n')
|
||||
|
||||
proxy_content = header + ip_content + proxy_content
|
||||
|
||||
return proxy_content
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
with open(args.input, 'rb') as f:
|
||||
content = f.read()
|
||||
content = decode_gfwlist(content)
|
||||
domains = parse_gfwlist(content)
|
||||
acl_content = generate_acl(domains)
|
||||
with open(args.output, 'wb') as f:
|
||||
f.write(acl_content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
7
client/android/shadowsocks/lint.xml
Normal file
7
client/android/shadowsocks/lint.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="MissingDefaultResource" severity="warning" />
|
||||
<issue id="MissingTranslation" severity="informational" />
|
||||
</lint>
|
||||
131
client/android/shadowsocks/src/main/AndroidManifest.xml
Normal file
131
client/android/shadowsocks/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.amnezia.vpn.shadowsocks.core"
|
||||
tools:ignore="MissingLeanbackLauncher">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:fullBackupOnly="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
tools:targetApi="n">
|
||||
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg" />
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:label="@string/app_name"-->
|
||||
<!-- android:permission="android.permission.BIND_VPN_SERVICE"-->
|
||||
<!-- android:process=":BG"-->
|
||||
<!-- tools:targetApi="n">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.net.VpnService" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </service>-->
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.TransproxyService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:process=":QtOnlyProcess"-->
|
||||
<!-- tools:targetApi="n" />-->
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.ProxyService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:process=":QtOnlyProcess"-->
|
||||
<!-- tools:targetApi="n" />-->
|
||||
|
||||
<!-- <activity-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.VpnRequestActivity"-->
|
||||
<!-- android:excludeFromRecents="true"-->
|
||||
<!-- android:launchMode="singleTask"-->
|
||||
<!-- android:taskAffinity=""-->
|
||||
<!-- android:theme="@style/Theme.AppCompat.Translucent" />-->
|
||||
|
||||
<receiver
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.BootReceiver"
|
||||
android:directBootAware="true"
|
||||
android:enabled="false"
|
||||
android:process=":QtOnlyProcess">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/work/workmanager/src/main/AndroidManifest.xml -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="androidx.work.impl.WorkManagerInitializer"
|
||||
tools:node="remove" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<service
|
||||
android:name="androidx.work.impl.background.systemjob.SystemJobService"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
|
||||
<receiver
|
||||
android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.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,18 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats;
|
||||
|
||||
//"oneway" unexpected. xinlake
|
||||
interface IShadowsocksServiceCallback {
|
||||
oneway void stateChanged(int state, String profileName, String msg);
|
||||
oneway void trafficUpdated(long profileId, in TrafficStats stats);
|
||||
// Traffic data has persisted to database, listener should refetch their data from database
|
||||
oneway void trafficPersisted(long profileId);
|
||||
}
|
||||
|
||||
//oneway interface IShadowsocksServiceCallback {
|
||||
// void stateChanged(int state, String profileName, String msg);
|
||||
// void trafficUpdated(long profileId, in TrafficStats stats);
|
||||
// // Traffic data has persisted to database, listener should refetch their data from database
|
||||
// void trafficPersisted(long profileId);
|
||||
//}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||
|
||||
parcelable TrafficStats;
|
||||
10377
client/android/shadowsocks/src/main/assets/acl/bypass-china.acl
Normal file
10377
client/android/shadowsocks/src/main/assets/acl/bypass-china.acl
Normal file
File diff suppressed because it is too large
Load diff
10391
client/android/shadowsocks/src/main/assets/acl/bypass-lan-china.acl
Normal file
10391
client/android/shadowsocks/src/main/assets/acl/bypass-lan-china.acl
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,17 @@
|
|||
[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/29
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.18.0.0/15
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/3
|
||||
5245
client/android/shadowsocks/src/main/assets/acl/china-list.acl
Normal file
5245
client/android/shadowsocks/src/main/assets/acl/china-list.acl
Normal file
File diff suppressed because it is too large
Load diff
5492
client/android/shadowsocks/src/main/assets/acl/gfwlist.acl
Normal file
5492
client/android/shadowsocks/src/main/assets/acl/gfwlist.acl
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,51 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
private val componentName by lazy { ComponentName(app, org.amnezia.vpn.shadowsocks.core.BootReceiver::class.java) }
|
||||
var enabled: Boolean
|
||||
get() = app.packageManager.getComponentEnabledSetting(org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName) ==
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
set(value) = app.packageManager.setComponentEnabledSetting(
|
||||
org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName,
|
||||
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val locked = when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> false
|
||||
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
|
||||
else -> return
|
||||
}
|
||||
if (DataStore.directBootAware == locked) org.amnezia.vpn.shadowsocks.core.Core.startService()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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 org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
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.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 org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.*
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
||||
import 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 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, it.udpFallback)
|
||||
}
|
||||
val currentProfile: Pair<Profile, Profile?>?
|
||||
get() {
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile()?.apply { return this }
|
||||
return ProfileManager.expand(ProfileManager.getProfile(DataStore.profileId)
|
||||
?: return null)
|
||||
}
|
||||
|
||||
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>) {
|
||||
Core.app = app
|
||||
configureIntent = {
|
||||
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
|
||||
val old = Acl.getFile(Acl.CUSTOM_RULES, app)
|
||||
if (old.canRead()) {
|
||||
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
||||
old.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
||||
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
||||
WorkManager.initialize(deviceStorage, Configuration.Builder().build())
|
||||
|
||||
// handle data restored/crash
|
||||
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
|
||||
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
|
||||
if (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(ContextCompat.getNoBackupFilesDir(deviceStorage), 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),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
|
||||
NotificationManager.IMPORTANCE_LOW)))
|
||||
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
||||
}
|
||||
}
|
||||
|
||||
fun getPackageInfo(packageName: String) = app.packageManager.getPackageInfo(packageName,
|
||||
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
||||
|
||||
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
class VpnManager private constructor() {
|
||||
|
||||
var state = BaseService.State.Idle
|
||||
private var context: Context? = null
|
||||
private val handler = Handler()
|
||||
private val connection = ShadowsocksConnection(handler, true)
|
||||
private var listener: OnStatusChangeListener? = null
|
||||
private val callback: ShadowsocksConnection.Callback = object : ShadowsocksConnection.Callback {
|
||||
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {
|
||||
changeState(state)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
|
||||
|
||||
override fun onServiceConnected(service: IShadowsocksService) {
|
||||
changeState(try {
|
||||
BaseService.State.values()[service.state]
|
||||
} catch (_: DeadObjectException) {
|
||||
BaseService.State.Idle
|
||||
})
|
||||
}
|
||||
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
super.trafficUpdated(profileId, stats)
|
||||
listener?.onTrafficUpdated(profileId, stats)
|
||||
}
|
||||
override fun onBinderDied() {
|
||||
disconnect()
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
context?.let {
|
||||
connection.connect(it, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
context?.let { connection.disconnect(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CONNECT = 1
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var instance: VpnManager? = null
|
||||
|
||||
fun getInstance(): VpnManager {
|
||||
if (instance == null) {
|
||||
instance = VpnManager()
|
||||
}
|
||||
return instance as VpnManager
|
||||
}
|
||||
}
|
||||
|
||||
fun init(context: Context){
|
||||
this.context=context
|
||||
connect()
|
||||
}
|
||||
|
||||
fun run() {
|
||||
when {
|
||||
state.canStop -> Core.stopService()
|
||||
// DataStore.serviceMode == Key.modeVpn -> {
|
||||
// val intent = VpnService.prepare(activity)
|
||||
// if (intent != null) activity.startActivityForResult(intent, REQUEST_CONNECT)
|
||||
// else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
|
||||
// }
|
||||
else -> Core.startService()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setOnStatusChangeListener(listener: OnStatusChangeListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
connection.bandwidthTimeout = 0
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
connection.bandwidthTimeout = 1000
|
||||
}
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when {
|
||||
requestCode != REQUEST_CONNECT -> {
|
||||
}
|
||||
resultCode == Activity.RESULT_OK -> Core.startService()
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeState(state: BaseService.State) {
|
||||
this.state = state
|
||||
this.listener?.onStatusChanged(state)
|
||||
}
|
||||
|
||||
interface OnStatusChangeListener {
|
||||
fun onStatusChanged(state: BaseService.State)
|
||||
|
||||
fun onTrafficUpdated(profileId: Long, stats: TrafficStats)
|
||||
}
|
||||
|
||||
enum class Route(name: String) {
|
||||
|
||||
ALL("all")
|
||||
|
||||
,
|
||||
BY_PASS_LAN("bypass-lan")
|
||||
|
||||
,
|
||||
BY_PASS_CHINA("bypass-china")
|
||||
|
||||
,
|
||||
BY_PASS_LAN_CHINA("bypass-lan-china")
|
||||
|
||||
,
|
||||
GFW_LIST("gfwlist")
|
||||
|
||||
,
|
||||
CHINA_LIST("china-list")
|
||||
|
||||
,
|
||||
CUSTOM_RULES("custom-rules");
|
||||
|
||||
var route = name
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
|
||||
class VpnRequestActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG = "VpnRequestActivity"
|
||||
private const val REQUEST_CONNECT = 1
|
||||
}
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (DataStore.serviceMode != Key.modeVpn) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
|
||||
receiver = broadcastReceiver { _, _ -> request() }
|
||||
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
|
||||
} else request()
|
||||
}
|
||||
|
||||
private fun request() {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
|
||||
else startActivityForResult(intent, REQUEST_CONNECT)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == RESULT_OK) Core.startService() else {
|
||||
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (receiver != null) unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
class Acl {
|
||||
companion object {
|
||||
const val TAG = "Acl"
|
||||
const val ALL = "all"
|
||||
const val BYPASS_LAN = "bypass-lan"
|
||||
const val BYPASS_CHN = "bypass-china"
|
||||
const val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||
const val GFWLIST = "gfwlist"
|
||||
const val CHINALIST = "china-list"
|
||||
const val CUSTOM_RULES = "custom-rules"
|
||||
|
||||
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||
|
||||
fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.noBackupFilesDir, "$id.acl")
|
||||
|
||||
var customRules: Acl
|
||||
get() {
|
||||
val acl = Acl()
|
||||
val str = DataStore.publicStore.getString(CUSTOM_RULES)
|
||||
if (str != null) acl.fromReader(str.reader(), true)
|
||||
if (!acl.bypass) {
|
||||
acl.bypass = true
|
||||
acl.subnets.clear()
|
||||
}
|
||||
return acl
|
||||
}
|
||||
set(value) = DataStore.publicStore.putString(CUSTOM_RULES,
|
||||
if ((!value.bypass || value.subnets.size() == 0) && value.bypassHostnames.size() == 0 &&
|
||||
value.proxyHostnames.size() == 0 && value.urls.size() == 0) null else value.toString())
|
||||
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
|
||||
}
|
||||
|
||||
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||
override fun onInserted(position: Int, count: Int) { }
|
||||
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) { }
|
||||
override fun onChanged(position: Int, count: Int) { }
|
||||
override fun onRemoved(position: Int, count: Int) { }
|
||||
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
|
||||
override fun compare(o1: T?, o2: T?): Int =
|
||||
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
|
||||
abstract fun compareNonNull(o1: T, o2: T): Int
|
||||
}
|
||||
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
|
||||
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
|
||||
}
|
||||
private object StringSorter : DefaultSorter<String>()
|
||||
private object SubnetSorter : DefaultSorter<Subnet>()
|
||||
private object URLSorter : BaseSorter<URL>() {
|
||||
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
|
||||
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
|
||||
}
|
||||
|
||||
val bypassHostnames = SortedList(String::class.java, StringSorter)
|
||||
val proxyHostnames = SortedList(String::class.java, StringSorter)
|
||||
val subnets = SortedList(Subnet::class.java, SubnetSorter)
|
||||
val urls = SortedList(URL::class.java, URLSorter)
|
||||
var bypass = false
|
||||
|
||||
fun fromAcl(other: Acl): Acl {
|
||||
bypassHostnames.clear()
|
||||
for (item in other.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||
proxyHostnames.clear()
|
||||
for (item in other.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||
subnets.clear()
|
||||
for (item in other.subnets.asIterable()) subnets.add(item)
|
||||
urls.clear()
|
||||
for (item in other.urls.asIterable()) urls.add(item)
|
||||
bypass = other.bypass
|
||||
return this
|
||||
}
|
||||
fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl {
|
||||
bypassHostnames.clear()
|
||||
proxyHostnames.clear()
|
||||
subnets.clear()
|
||||
urls.clear()
|
||||
bypass = defaultBypass
|
||||
val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||
val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||
var hostnames: SortedList<String>? = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||
var subnets: SortedList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
|
||||
reader.useLines {
|
||||
for (line in it) {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
val blocks = (line as java.lang.String).split("#", 2)
|
||||
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
|
||||
if (url != null) urls.add(URL(url))
|
||||
when (val input = blocks[0].trim()) {
|
||||
"[outbound_block_list]" -> {
|
||||
hostnames = null
|
||||
subnets = null
|
||||
}
|
||||
"[black_list]", "[bypass_list]" -> {
|
||||
hostnames = bypassHostnames
|
||||
subnets = bypassSubnets
|
||||
}
|
||||
"[white_list]", "[proxy_list]" -> {
|
||||
hostnames = proxyHostnames
|
||||
subnets = proxySubnets
|
||||
}
|
||||
"[reject_all]", "[bypass_all]" -> bypass = true
|
||||
"[accept_all]", "[proxy_all]" -> bypass = false
|
||||
else -> if (subnets != null && input.isNotEmpty()) {
|
||||
val subnet = Subnet.fromString(input)
|
||||
if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
|
||||
return this
|
||||
}
|
||||
|
||||
fun fromId(id: String): Acl = try {
|
||||
fromReader(getFile(id).bufferedReader())
|
||||
} catch (_: IOException) { this }
|
||||
|
||||
suspend fun flatten(depth: Int, connect: suspend (URL) -> URLConnection): Acl {
|
||||
if (depth > 0) for (url in urls.asIterable()) {
|
||||
val child = Acl()
|
||||
try {
|
||||
child.fromReader(connect(url).getInputStream().bufferedReader(), bypass).flatten(depth - 1, connect)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
continue
|
||||
}
|
||||
if (bypass != child.bypass) {
|
||||
child.subnets.clear() // subnets for the different mode are discarded
|
||||
child.bypass = bypass
|
||||
}
|
||||
for (item in child.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||
for (item in child.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||
for (item in child.subnets.asIterable()) subnets.add(item)
|
||||
}
|
||||
urls.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val result = StringBuilder()
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
val bypassList = (if (bypass) {
|
||||
bypassHostnames.asIterable().asSequence()
|
||||
} else {
|
||||
subnets.asIterable().asSequence().map(Subnet::toString) + bypassHostnames.asIterable().asSequence()
|
||||
}).toList()
|
||||
val proxyList = (if (bypass) {
|
||||
subnets.asIterable().asSequence().map(Subnet::toString) + proxyHostnames.asIterable().asSequence()
|
||||
} else {
|
||||
proxyHostnames.asIterable().asSequence()
|
||||
}).toList()
|
||||
if (bypassList.isNotEmpty()) {
|
||||
result.append("[bypass_list]\n")
|
||||
result.append(bypassList.joinToString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
if (proxyList.isNotEmpty()) {
|
||||
result.append("[proxy_list]\n")
|
||||
result.append(proxyList.joinToString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" })
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||
companion object {
|
||||
private const val KEY_ROUTE = "route"
|
||||
|
||||
fun schedule(route: String) = WorkManager.getInstance().enqueueUniqueWork(route, ExistingWorkPolicy.REPLACE,
|
||||
OneTimeWorkRequestBuilder<AclSyncer>().run {
|
||||
setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
|
||||
setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setRequiresCharging(true)
|
||||
.build())
|
||||
setInitialDelay(10, TimeUnit.SECONDS)
|
||||
build()
|
||||
})
|
||||
}
|
||||
|
||||
override val coroutineContext get() = Dispatchers.IO
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val route = inputData.getString(KEY_ROUTE)!!
|
||||
val acl = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openStream().bufferedReader()
|
||||
.use { it.readText() }
|
||||
Acl.getFile(route).printWriter().use { it.write(acl) }
|
||||
Result.success()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.TransproxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
/**
|
||||
* This object should be compact as it will not get GC-ed.
|
||||
*/
|
||||
class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
private var listenForDeath: Boolean = false) :
|
||||
ServiceConnection, IBinder.DeathRecipient {
|
||||
companion object {
|
||||
val serviceClass get() = when (DataStore.serviceMode) {
|
||||
Key.modeProxy -> ProxyService::class
|
||||
Key.modeVpn -> ShadowsocksVpnService::class
|
||||
Key.modeTransproxy -> TransproxyService::class
|
||||
else -> throw UnknownError()
|
||||
}.java
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
||||
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
|
||||
fun trafficPersisted(profileId: Long) { }
|
||||
|
||||
fun 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) {
|
||||
val service = service
|
||||
if (bandwidthTimeout != value && service != null)
|
||||
if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
|
||||
service.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: DeadObjectException) { }
|
||||
field = value
|
||||
}
|
||||
var service: IShadowsocksService? = null
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||
this.binder = binder
|
||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||
this.service = service
|
||||
if (!callbackRegistered) try {
|
||||
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
|
||||
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
|
||||
service?.stopListeningForBandwidth(serviceCallback)
|
||||
service = null
|
||||
callback = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.*
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||
import java.io.File
|
||||
import java.net.BindException
|
||||
import java.net.InetAddress
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* 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"
|
||||
|
||||
class Data(private val service: Interface) {
|
||||
var state = State.Stopped
|
||||
var processes: GuardedProcessPool? = null
|
||||
var proxy: ProxyInstance? = null
|
||||
var udpFallback: ProxyInstance? = null
|
||||
|
||||
// var notification: ServiceNotification? = null
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
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(), AutoCloseable {
|
||||
val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
|
||||
super.onCallbackDied(callback, cookie)
|
||||
stopListeningForBandwidth(callback ?: return)
|
||||
}
|
||||
}
|
||||
private val bandwidthListeners =
|
||||
mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
private val handler = Handler()
|
||||
|
||||
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) {
|
||||
repeat(callbacks.beginBroadcast()) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: DeadObjectException) {
|
||||
} catch (e: Exception) {
|
||||
printLog(e)
|
||||
}
|
||||
}
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
|
||||
private fun registerTimeout() {
|
||||
handler.postDelayed(this::onTimeout, bandwidthListeners.values.minOrNull() ?: return)
|
||||
}
|
||||
|
||||
private fun onTimeout() {
|
||||
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||
val stats = proxies
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) {
|
||||
stats.forEach { (id, stats) -> item.trafficUpdated(id, stats) }
|
||||
item.trafficUpdated(0, sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
registerTimeout()
|
||||
}
|
||||
|
||||
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
|
||||
val wasEmpty = bandwidthListeners.isEmpty()
|
||||
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
|
||||
if (wasEmpty) registerTimeout()
|
||||
if (data?.state != State.Connected) return
|
||||
var sum = TrafficStats()
|
||||
val data = data
|
||||
val proxy = data?.proxy ?: return
|
||||
proxy.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(
|
||||
proxy.profile.id, if (stats == null) sum else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
}
|
||||
data.udpFallback?.also { udpFallback ->
|
||||
udpFallback.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(
|
||||
udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
cb.trafficUpdated(0, sum)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||
handler.removeCallbacksAndMessages(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()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
|
||||
|
||||
suspend fun startProcesses() {
|
||||
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||
?.isUserUnlocked != false
|
||||
) app else Core.deviceStorage).noBackupFilesDir
|
||||
val udpFallback = data.udpFallback
|
||||
data.proxy!!.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
if (udpFallback == null) "-u" else null
|
||||
)
|
||||
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
|
||||
udpFallback?.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||
File(configRoot, CONFIG_FILE_UDP),
|
||||
"-U"
|
||||
)
|
||||
}
|
||||
|
||||
fun startRunner() {
|
||||
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) {
|
||||
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, data.udpFallback).map {
|
||||
it.shutdown(this)
|
||||
it.profile.id
|
||||
}
|
||||
data.proxy = null
|
||||
data.udpFallback = 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 {
|
||||
Log.d("Aman", "Stop Self BaseService-------")
|
||||
// stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun preInit() {}
|
||||
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
|
||||
suspend fun openConnection(url: URL) = url.openConnection()
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val data = data
|
||||
if (data.state != State.Stopped) return Service.START_REDELIVER_INTENT
|
||||
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_REDELIVER_INTENT
|
||||
}
|
||||
val (profile, fallback) = profilePair
|
||||
profile.name = profile.formattedName // save name for later queries
|
||||
val proxy = ProxyInstance(profile)
|
||||
data.proxy = proxy
|
||||
data.udpFallback =
|
||||
if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
|
||||
if (!data.closeReceiverRegistered) {
|
||||
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||
addAction(Action.RELOAD)
|
||||
addAction(Intent.ACTION_SHUTDOWN)
|
||||
addAction(Action.CLOSE)
|
||||
})
|
||||
data.closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
// data.notification = createNotification(profile.formattedName)
|
||||
|
||||
data.changeState(State.Connecting)
|
||||
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
Executable.killAll() // clean up old processes
|
||||
preInit()
|
||||
proxy.init(this@Interface)
|
||||
data.udpFallback?.init(this@Interface)
|
||||
|
||||
data.processes = GuardedProcessPool {
|
||||
printLog(it)
|
||||
stopRunner(false, it.readableMessage)
|
||||
}
|
||||
startProcesses()
|
||||
|
||||
proxy.scheduleUpdate()
|
||||
data.udpFallback?.scheduleUpdate()
|
||||
|
||||
data.changeState(State.Connected)
|
||||
} catch (_: CancellationException) {
|
||||
// if the job was cancelled, it is canceller's responsibility to call stopRunner
|
||||
} catch (_: UnknownHostException) {
|
||||
stopRunner(false, getString(R.string.invalid_server))
|
||||
} catch (exc: Throwable) {
|
||||
if (exc !is PluginManager.PluginNotFoundException &&
|
||||
exc !is BindException &&
|
||||
exc !is ShadowsocksVpnService.NullConnectionException
|
||||
) {
|
||||
printLog(exc)
|
||||
}
|
||||
stopRunner(
|
||||
false,
|
||||
"${getString(R.string.service_failed)}: ${exc.readableMessage}"
|
||||
)
|
||||
} finally {
|
||||
data.connectingJob = null
|
||||
}
|
||||
}
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.text.TextUtils
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object Executable {
|
||||
const val REDSOCKS = "libredsocks.so"
|
||||
const val SS_LOCAL = "libss-local.so"
|
||||
const val TUN2SOCKS = "libtun2socks.so"
|
||||
|
||||
private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import androidx.annotation.MainThread
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.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) { Log.e(cmdName, it) } }
|
||||
thread(name = "stdout-$cmdName") {
|
||||
streamLogger(process.inputStream) { Log.i(cmdName, it) }
|
||||
// this thread also acts as a daemon thread for waitFor
|
||||
runBlocking { exitChannel.send(process.waitFor()) }
|
||||
}
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
val exitCode = exitChannel.receive()
|
||||
running = false
|
||||
if (SystemClock.elapsedRealtime() - startTime < 1000) {
|
||||
throw IOException("$cmdName exits too fast (exit code: $exitCode)")
|
||||
}
|
||||
start()
|
||||
onRestartCallback?.invoke()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
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) {
|
||||
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,70 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Socks5Endpoint
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.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 chinaIpList by lazy {
|
||||
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
|
||||
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
|
||||
}
|
||||
|
||||
private val servers = WeakHashMap<Interface, LocalDnsServer>()
|
||||
|
||||
interface Interface : BaseService.Interface {
|
||||
override suspend fun startProcesses() {
|
||||
super.startProcesses()
|
||||
val profile = data.proxy!!.profile
|
||||
val dns = URI("dns://${profile.remoteDns}")
|
||||
LocalDnsServer(this::resolver,
|
||||
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||
DataStore.proxyAddress).apply {
|
||||
tcp = !profile.udpdns
|
||||
when (profile.route) {
|
||||
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
|
||||
remoteDomainMatcher = googleApisTester
|
||||
localIpMatcher = chinaIpList
|
||||
}
|
||||
Acl.CHINALIST -> { }
|
||||
else -> forwardOnly = true
|
||||
}
|
||||
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
|
||||
}
|
||||
|
||||
override fun killProcesses(scope: CoroutineScope) {
|
||||
servers.remove(this)?.shutdown(scope)
|
||||
super.killProcesses(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.content.Context
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.AclSyncer
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* This class sets up environment for ss-local.
|
||||
*/
|
||||
class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
|
||||
private var configFile: File? = null
|
||||
var trafficMonitor: TrafficMonitor? = null
|
||||
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
|
||||
val pluginPath by lazy { PluginManager.init(plugin) }
|
||||
|
||||
suspend fun init(service: BaseService.Interface) {
|
||||
if (route == Acl.CUSTOM_RULES) withContext(Dispatchers.IO) {
|
||||
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, service::openConnection))
|
||||
}
|
||||
|
||||
// it's hard to resolve DNS on a specific interface so we'll do it here
|
||||
if (profile.host.parseNumericAddress() == null) {
|
||||
while (true) try {
|
||||
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
|
||||
profile.host = io.await().firstOrNull()?.hostAddress ?: throw UnknownHostException()
|
||||
return
|
||||
} catch (e: UnknownHostException) {
|
||||
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
|
||||
if (!DataStore.hasArc0) throw e
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
|
||||
* device storage, depending on which is currently available.
|
||||
*/
|
||||
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
|
||||
trafficMonitor = TrafficMonitor(stat)
|
||||
|
||||
this.configFile = configFile
|
||||
val config = profile.toJson()
|
||||
if (pluginPath != null) config.put("plugin", pluginPath).put("plugin_opts", plugin.toString())
|
||||
configFile.writeText(config.toString())
|
||||
|
||||
val cmd = service.buildAdditionalArguments(arrayListOf(
|
||||
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||
"-b", DataStore.listenAddress,
|
||||
"-l", DataStore.portProxy.toString(),
|
||||
"-t", "600",
|
||||
"-S", stat.absolutePath,
|
||||
"-c", configFile.absolutePath))
|
||||
if (extraFlag != null) cmd.add(extraFlag)
|
||||
|
||||
if (route != Acl.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += Acl.getFile(route).absolutePath
|
||||
}
|
||||
|
||||
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
|
||||
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_LAN) cmd += "-D"
|
||||
|
||||
if (DataStore.tcpFastOpen) cmd += "--fast-open"
|
||||
|
||||
service.data.processes!!.start(cmd)
|
||||
}
|
||||
|
||||
fun scheduleUpdate() {
|
||||
if (route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncer.schedule(route)
|
||||
}
|
||||
|
||||
fun shutdown(scope: CoroutineScope) {
|
||||
trafficMonitor?.apply {
|
||||
thread.shutdown(scope)
|
||||
// Make sure update total traffic when stopping the runner
|
||||
try {
|
||||
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
||||
val profile = ProfileManager.getProfile(profile.id) ?: return
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
ProfileManager.updateProfile(profile)
|
||||
} catch (e: IOException) {
|
||||
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
|
||||
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == profile.id }
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
profile.dirty = true
|
||||
DirectBoot.update(profile)
|
||||
DirectBoot.listenForUnlock()
|
||||
}
|
||||
}
|
||||
trafficMonitor = null
|
||||
configFile?.delete() // remove old config possibly in device storage
|
||||
configFile = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Shadowsocks service at its minimum.
|
||||
*/
|
||||
class ProxyService : Service(), BaseService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksProxyService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-proxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
data.binder.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
///*******************************************************************************
|
||||
// * *
|
||||
// * Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
// * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
// * *
|
||||
// * This program is free software: you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
// * *
|
||||
// *******************************************************************************/
|
||||
//
|
||||
//package org.amnezia.vpn.shadowsocks.core.bg
|
||||
//
|
||||
//import android.app.KeyguardManager
|
||||
//import android.app.NotificationManager
|
||||
//import android.app.PendingIntent
|
||||
//import android.app.Service
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.content.IntentFilter
|
||||
//import android.os.Build
|
||||
//import android.os.PowerManager
|
||||
//import android.text.format.Formatter
|
||||
//import androidx.core.app.NotificationCompat
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.core.content.getSystemService
|
||||
//import org.amnezia.vpn.shadowsocks.core.Core
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
//import org.amnezia.vpn.shadowsocks.core.R
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
//
|
||||
///**
|
||||
// * Android < 8 VPN: always invisible because of VPN notification/icon
|
||||
// * Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||
// * Android 8+: always visible due to system limitations
|
||||
// * (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||
// */
|
||||
//class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
// channel: String, private val visible: Boolean = false) {
|
||||
// private val keyGuard = (service as Context).getSystemService<KeyguardManager>()!!
|
||||
// private val nm by lazy { (service as Context).getSystemService<NotificationManager>()!! }
|
||||
// private val callback: IShadowsocksServiceCallback by lazy {
|
||||
// object : IShadowsocksServiceCallback.Stub() {
|
||||
// override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
// when (state) {
|
||||
// BaseService.State.Connected.ordinal -> {
|
||||
// builder.setContentText("VPN Connected")
|
||||
// }
|
||||
// BaseService.State.Stopped.ordinal -> {
|
||||
// builder.setContentText("VPN Disconnected")
|
||||
// }
|
||||
// }
|
||||
// } // ignore
|
||||
// override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
//// if (profileId != 0L) return
|
||||
//// service as Context
|
||||
//// val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate))
|
||||
//// val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))
|
||||
//// builder.setContentText("$txr↑\t$rxr↓")
|
||||
//// style.bigText(service.getString(R.string.stat_summary, txr, rxr,
|
||||
//// Formatter.formatFileSize(service, stats.txTotal),
|
||||
//// Formatter.formatFileSize(service, stats.rxTotal)))
|
||||
//// show()
|
||||
// }
|
||||
// override fun trafficPersisted(profileId: Long) { }
|
||||
// }
|
||||
// }
|
||||
//// private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||
// private var callbackRegistered = false
|
||||
//
|
||||
// private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
// .setWhen(0)
|
||||
// .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
// .setTicker(service.getString(R.string.forward_success))
|
||||
// .setContentTitle("AmneziaVPN -- testing")
|
||||
// .setContentIntent(Core.configureIntent(service))
|
||||
// .setSmallIcon(R.drawable.ic_amnezia_round)
|
||||
// private val style = NotificationCompat.BigTextStyle(builder).bigText("")
|
||||
// private var isVisible = true
|
||||
//
|
||||
// init {
|
||||
// service as Context
|
||||
//// if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
//// service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
//// update(if (service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||
//// Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
//// service.registerReceiver(lockReceiver, IntentFilter().apply {
|
||||
//// addAction(Intent.ACTION_SCREEN_ON)
|
||||
//// addAction(Intent.ACTION_SCREEN_OFF)
|
||||
//// if (visible && Build.VERSION.SDK_INT < 26) addAction(Intent.ACTION_USER_PRESENT)
|
||||
//// })
|
||||
// }
|
||||
//
|
||||
//// private fun update(action: String?, forceShow: Boolean = false) {
|
||||
//// if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
|
||||
//// Intent.ACTION_SCREEN_OFF -> {
|
||||
//// setVisible(false, forceShow)
|
||||
//// unregisterCallback() // unregister callback to save battery
|
||||
//// }
|
||||
//// Intent.ACTION_SCREEN_ON -> {
|
||||
//// setVisible(visible && !keyGuard.isKeyguardLocked, forceShow)
|
||||
//// service.data.binder.registerCallback(callback)
|
||||
//// service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||
//// callbackRegistered = true
|
||||
//// }
|
||||
//// Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// private fun unregisterCallback() {
|
||||
// if (callbackRegistered) {
|
||||
// service.data.binder.unregisterCallback(callback)
|
||||
// callbackRegistered = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||
// if (isVisible != visible) {
|
||||
// isVisible = visible
|
||||
// builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||
// show()
|
||||
// } else if (forceShow) show()
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private fun show() = (service as Service).startForeground(1337, builder.build())
|
||||
//
|
||||
// fun destroy() {
|
||||
//// (service as Service).unregisterReceiver(lockReceiver)
|
||||
// unregisterCallback()
|
||||
//// service.stopForeground(true)
|
||||
// nm.cancel(1337)
|
||||
// }
|
||||
//}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
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 android.util.Log
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
|
||||
open class ShadowsocksVpnService : 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() {
|
||||
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
||||
}
|
||||
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksVpnService"
|
||||
|
||||
val NOTIFICATION_CHANNEL_ID = "com.amnezia.vpnNotification"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, NOTIFICATION_CHANNEL_ID)
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
private var active = false
|
||||
private var metered = false
|
||||
private var underlyingNetwork: Network? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
|
||||
}
|
||||
private val underlyingNetworks
|
||||
get() =
|
||||
// clearing underlyingNetworks makes Android 9+ consider the network to be metered
|
||||
if (Build.VERSION.SDK_INT >= 28 && metered) null else underlyingNetwork?.let { arrayOf(it) }
|
||||
|
||||
override fun onBind(intent: Intent) = when (intent.action) {
|
||||
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
||||
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 (DataStore.serviceMode == Key.modeVpn) {
|
||||
if (prepare(this) != null) {
|
||||
startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
stopRunner()
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
|
||||
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
|
||||
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
|
||||
|
||||
override suspend fun startProcesses() {
|
||||
worker = ProtectWorker().apply { start() }
|
||||
super.startProcesses()
|
||||
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)
|
||||
}
|
||||
val me = packageName
|
||||
if (profile.proxyApps) {
|
||||
profile.individual.split('\n')
|
||||
.filter { it != me }
|
||||
.forEach {
|
||||
try {
|
||||
if (profile.bypass) builder.addDisallowedApplication(it)
|
||||
else builder.addAllowedApplication(it)
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
printLog(ex)
|
||||
}
|
||||
}
|
||||
if (profile.bypass) {
|
||||
builder.addDisallowedApplication(me)
|
||||
}
|
||||
} else {
|
||||
builder.addDisallowedApplication(me)
|
||||
}
|
||||
|
||||
when (profile.route) {
|
||||
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
|
||||
else -> {
|
||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||
val subnet = Subnet.fromString(it)!!
|
||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||
}
|
||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
||||
}
|
||||
}
|
||||
|
||||
metered = profile.metered
|
||||
active = true // possible race condition here?
|
||||
if (Build.VERSION.SDK_INT >= 22) builder.setUnderlyingNetworks(underlyingNetworks)
|
||||
|
||||
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) {
|
||||
e.printStackTrace()
|
||||
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,82 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.os.SystemClock
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import java.io.File
|
||||
|
||||
class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText(
|
||||
"""base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
redsocks {
|
||||
local_ip = ${DataStore.listenAddress};
|
||||
local_port = ${DataStore.portTransproxy};
|
||||
ip = 127.0.0.1;
|
||||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
"""
|
||||
)
|
||||
data.processes!!.start(
|
||||
listOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c",
|
||||
"redsocks.conf"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun startProcesses() {
|
||||
startRedsocksDaemon()
|
||||
super.startProcesses()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
data.binder.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import 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()
|
||||
for (v in value) {
|
||||
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
|
||||
stream.write(v.toByteArray())
|
||||
}
|
||||
this.value = stream.toByteArray()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
@Database(entities = [Profile::class, KeyValuePair::class], version = 29)
|
||||
abstract class PrivateDatabase : RoomDatabase() {
|
||||
companion object {
|
||||
private val instance by lazy {
|
||||
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
|
||||
.addMigrations(
|
||||
Migration26,
|
||||
Migration27,
|
||||
Migration28
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
|
||||
val profileDao get() = instance.profileDao()
|
||||
val kvPairDao get() = instance.keyValuePairDao()
|
||||
}
|
||||
abstract fun profileDao(): Profile.Dao
|
||||
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||
|
||||
object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
|
||||
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)",
|
||||
"`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
super.migrate(database)
|
||||
PublicDatabase.Migration3.migrate(database)
|
||||
}
|
||||
}
|
||||
object Migration27 : Migration(26, 27) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) =
|
||||
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `udpFallback` INTEGER")
|
||||
}
|
||||
object Migration28 : Migration(27, 28) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) =
|
||||
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `metered` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.util.LongSparseArray
|
||||
import androidx.core.net.toUri
|
||||
import androidx.room.*
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.json.JSONTokener
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
@Parcelize
|
||||
data class Profile(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
var name: String? = "",
|
||||
var host: String = "155.94.174.51",
|
||||
var remotePort: Int = 444,
|
||||
var password: String = "789456123",
|
||||
var method: String = "aes-256-cfb",
|
||||
var route: String = "all",
|
||||
var remoteDns: String = "dns.google",
|
||||
var proxyApps: Boolean = false,
|
||||
var bypass: Boolean = false,
|
||||
var udpdns: Boolean = false,
|
||||
var ipv6: Boolean = true,
|
||||
@TargetApi(28)
|
||||
var metered: Boolean = false,
|
||||
var individual: String = "",
|
||||
var tx: Long = 0,
|
||||
var rx: Long = 0,
|
||||
var userOrder: Long = 0,
|
||||
var plugin: String? = null,
|
||||
var udpFallback: Long? = null,
|
||||
|
||||
@Ignore // not persisted in db, only used by direct boot
|
||||
var dirty: Boolean = false
|
||||
) : Parcelable, Serializable {
|
||||
companion object {
|
||||
private const val TAG = "ShadowParser"
|
||||
private const val serialVersionUID = 1L
|
||||
private val pattern =
|
||||
"""(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
|
||||
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
|
||||
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
|
||||
|
||||
fun findAllUrls(data: CharSequence?, feature: Profile? = null) = pattern.findAll(data ?: "").map {
|
||||
val uri = it.value.toUri()
|
||||
try {
|
||||
if (uri.userInfo == null) {
|
||||
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
|
||||
if (match != null) {
|
||||
val profile = Profile()
|
||||
feature?.copyFeatureSettingsTo(profile)
|
||||
profile.method = match.groupValues[1].toLowerCase()
|
||||
profile.password = match.groupValues[2]
|
||||
profile.host = match.groupValues[3]
|
||||
profile.remotePort = match.groupValues[4].toInt()
|
||||
profile.plugin = uri.getQueryParameter(Key.plugin)
|
||||
profile.name = uri.fragment
|
||||
profile
|
||||
} else {
|
||||
Log.e(TAG, "Unrecognized URI: ${it.value}")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
|
||||
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
|
||||
if (match != null) {
|
||||
val profile = Profile()
|
||||
feature?.copyFeatureSettingsTo(profile)
|
||||
profile.method = match.groupValues[1]
|
||||
profile.password = match.groupValues[2]
|
||||
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
|
||||
try {
|
||||
val javaURI = URI(it.value)
|
||||
profile.host = javaURI.host ?: ""
|
||||
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
|
||||
profile.host = profile.host.substring(1, profile.host.length - 1)
|
||||
profile.remotePort = javaURI.port
|
||||
profile.plugin = uri.getQueryParameter(Key.plugin)
|
||||
profile.name = uri.fragment ?: ""
|
||||
profile
|
||||
} catch (e: URISyntaxException) {
|
||||
Log.e(TAG, "Invalid URI: ${it.value}")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Unknown user info: ${it.value}")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Invalid base64 detected: ${it.value}")
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
|
||||
private class JsonParser(private val feature: Profile? = null) : ArrayList<Profile>() {
|
||||
private val fallbackMap = mutableMapOf<Profile, Profile>()
|
||||
|
||||
private fun tryParse(json: JSONObject, fallback: Boolean = false): Profile? {
|
||||
val host = json.optString("server")
|
||||
if (host.isNullOrEmpty()) return null
|
||||
val remotePort = json.optInt("server_port")
|
||||
if (remotePort <= 0) return null
|
||||
val password = json.optString("password")
|
||||
if (password.isNullOrEmpty()) return null
|
||||
val method = json.optString("method")
|
||||
if (method.isNullOrEmpty()) return null
|
||||
return Profile().also {
|
||||
it.host = host
|
||||
it.remotePort = remotePort
|
||||
it.password = password
|
||||
it.method = method
|
||||
}.apply {
|
||||
feature?.copyFeatureSettingsTo(this)
|
||||
val id = json.optString("plugin")
|
||||
if (!id.isNullOrEmpty()) {
|
||||
plugin = PluginOptions(id, json.optString("plugin_opts")).toString(false)
|
||||
}
|
||||
name = json.optString("remarks")
|
||||
route = json.optString("route", route)
|
||||
if (fallback) return@apply
|
||||
remoteDns = json.optString("remote_dns", remoteDns)
|
||||
ipv6 = json.optBoolean("ipv6", ipv6)
|
||||
metered = json.optBoolean("metered", metered)
|
||||
json.optJSONObject("proxy_apps")?.also {
|
||||
proxyApps = it.optBoolean("enabled", proxyApps)
|
||||
bypass = it.optBoolean("bypass", bypass)
|
||||
individual = it.optJSONArray("android_list")?.asIterable()?.joinToString("\n") ?: individual
|
||||
}
|
||||
udpdns = json.optBoolean("udpdns", udpdns)
|
||||
json.optJSONObject("udp_fallback")?.let { tryParse(it, true) }?.also { fallbackMap[this] = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun process(json: Any) {
|
||||
when (json) {
|
||||
is JSONObject -> {
|
||||
val profile = tryParse(json)
|
||||
if (profile != null) add(profile) else for (key in json.keys()) process(json.get(key))
|
||||
}
|
||||
is JSONArray -> json.asIterable().forEach(this::process)
|
||||
// ignore other types
|
||||
}
|
||||
}
|
||||
fun finalize(create: (Profile) -> Unit) {
|
||||
val profiles = ProfileManager.getAllProfiles() ?: emptyList()
|
||||
for ((profile, fallback) in fallbackMap) {
|
||||
val match = profiles.firstOrNull {
|
||||
fallback.host == it.host && fallback.remotePort == it.remotePort &&
|
||||
fallback.password == it.password && fallback.method == it.method &&
|
||||
it.plugin.isNullOrEmpty()
|
||||
}
|
||||
profile.udpFallback = if (match == null) {
|
||||
create(fallback)
|
||||
fallback.id
|
||||
} else match.id
|
||||
ProfileManager.updateProfile(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun parseJson(json: String, feature: Profile? = null, create: (Profile) -> Unit) = JsonParser(feature).run {
|
||||
process(JSONTokener(json).nextValue())
|
||||
for (profile in this) create(profile)
|
||||
finalize(create)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
|
||||
operator fun get(id: Long): Profile?
|
||||
|
||||
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
|
||||
fun list(): List<Profile>
|
||||
|
||||
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
|
||||
fun nextOrder(): Long?
|
||||
|
||||
@Query("SELECT 1 FROM `Profile` LIMIT 1")
|
||||
fun isNotEmpty(): Boolean
|
||||
|
||||
@Insert
|
||||
fun create(value: Profile): Long
|
||||
|
||||
@Update
|
||||
fun update(value: Profile): Int
|
||||
|
||||
@Query("DELETE FROM `Profile` WHERE `id` = :id")
|
||||
fun delete(id: Long): Int
|
||||
|
||||
@Query("DELETE FROM `Profile`")
|
||||
fun deleteAll(): Int
|
||||
}
|
||||
|
||||
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
|
||||
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
|
||||
|
||||
fun copyFeatureSettingsTo(profile: Profile) {
|
||||
profile.route = route
|
||||
profile.ipv6 = ipv6
|
||||
profile.metered = metered
|
||||
profile.proxyApps = proxyApps
|
||||
profile.bypass = bypass
|
||||
profile.individual = individual
|
||||
profile.udpdns = udpdns
|
||||
}
|
||||
|
||||
fun toUri(): Uri {
|
||||
val auth = Base64.encodeToString("$method:$password".toByteArray(),
|
||||
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
|
||||
val wrappedHost = if (host.contains(':')) "[$host]" else host
|
||||
val builder = Uri.Builder()
|
||||
.scheme("ss")
|
||||
.encodedAuthority("$auth@$wrappedHost:$remotePort")
|
||||
val configuration = PluginConfiguration(plugin ?: "")
|
||||
if (configuration.selected.isNotEmpty())
|
||||
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
|
||||
if (!name.isNullOrEmpty()) builder.fragment(name)
|
||||
return builder.build()
|
||||
}
|
||||
override fun toString() = toUri().toString()
|
||||
|
||||
fun toJson(profiles: LongSparseArray<Profile>? = null): JSONObject = JSONObject().apply {
|
||||
put("server", host)
|
||||
put("server_port", remotePort)
|
||||
put("password", password)
|
||||
put("method", method)
|
||||
if (profiles == null) return@apply
|
||||
PluginConfiguration(plugin ?: "").selectedOptions.also {
|
||||
if (it.id.isNotEmpty()) {
|
||||
put("plugin", it.id)
|
||||
put("plugin_opts", it.toString())
|
||||
}
|
||||
}
|
||||
put("remarks", name)
|
||||
put("route", route)
|
||||
put("remote_dns", remoteDns)
|
||||
put("ipv6", ipv6)
|
||||
put("metered", metered)
|
||||
put("proxy_apps", JSONObject().apply {
|
||||
put("enabled", proxyApps)
|
||||
if (proxyApps) {
|
||||
put("bypass", bypass)
|
||||
// android_ prefix is used because package names are Android specific
|
||||
put("android_list", JSONArray(individual.split("\n")))
|
||||
}
|
||||
})
|
||||
put("udpdns", udpdns)
|
||||
val fallback = profiles.get(udpFallback ?: return@apply)
|
||||
if (fallback != null && fallback.plugin.isNullOrEmpty()) fallback.toJson().also { put("udp_fallback", it) }
|
||||
}
|
||||
|
||||
fun serialize() {
|
||||
DataStore.editingId = id
|
||||
DataStore.privateStore.putString(Key.name, name)
|
||||
DataStore.privateStore.putString(Key.host, host)
|
||||
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
|
||||
DataStore.privateStore.putString(Key.password, password)
|
||||
DataStore.privateStore.putString(Key.route, route)
|
||||
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
|
||||
DataStore.privateStore.putString(Key.method, method)
|
||||
DataStore.proxyApps = proxyApps
|
||||
DataStore.bypass = bypass
|
||||
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
|
||||
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
|
||||
DataStore.privateStore.putBoolean(Key.metered, metered)
|
||||
DataStore.individual = individual
|
||||
DataStore.plugin = plugin ?: ""
|
||||
DataStore.udpFallback = udpFallback
|
||||
DataStore.privateStore.remove(Key.dirty)
|
||||
}
|
||||
fun deserialize() {
|
||||
check(id == 0L || DataStore.editingId == id)
|
||||
DataStore.editingId = null
|
||||
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
|
||||
name = DataStore.privateStore.getString(Key.name) ?: ""
|
||||
host = DataStore.privateStore.getString(Key.host) ?: ""
|
||||
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
|
||||
password = DataStore.privateStore.getString(Key.password) ?: ""
|
||||
method = DataStore.privateStore.getString(Key.method) ?: ""
|
||||
route = DataStore.privateStore.getString(Key.route) ?: ""
|
||||
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
|
||||
proxyApps = DataStore.proxyApps
|
||||
bypass = DataStore.bypass
|
||||
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
|
||||
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
|
||||
metered = DataStore.privateStore.getBoolean(Key.metered, false)
|
||||
individual = DataStore.individual
|
||||
plugin = DataStore.plugin
|
||||
udpFallback = DataStore.udpFallback
|
||||
}
|
||||
}
|
||||
|
|
@ -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 org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import android.database.sqlite.SQLiteCantOpenDatabaseException
|
||||
import android.util.LongSparseArray
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.json.JSONArray
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
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() }
|
||||
var result: Exception? = null
|
||||
for (json in jsons) try {
|
||||
Profile.parseJson(json.bufferedReader().readText(), feature) {
|
||||
if (replace) {
|
||||
lazyClear.value
|
||||
// if two profiles has the same address, treat them as the same profile and copy stats over
|
||||
profiles?.get(it.formattedAddress)?.apply {
|
||||
it.tx = tx
|
||||
it.rx = rx
|
||||
}
|
||||
}
|
||||
createProfile(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (result == null) result = e else result.addSuppressed(e)
|
||||
}
|
||||
if (result != null) throw result
|
||||
}
|
||||
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
|
||||
if (profiles == null) return null
|
||||
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
|
||||
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, profile.udpFallback?.let { getProfile(it) })
|
||||
|
||||
@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,50 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
@Database(entities = [KeyValuePair::class], version = 4)
|
||||
abstract class PublicDatabase : RoomDatabase() {
|
||||
companion object {
|
||||
private val instance by lazy {
|
||||
Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
|
||||
.allowMainThreadQueries()
|
||||
.addMigrations(
|
||||
Migration3
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
val kvPairDao get() = instance.keyValuePairDao()
|
||||
}
|
||||
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||
|
||||
internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
|
||||
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"`key`, `valueType`, `value`")
|
||||
}
|
||||
|
|
@ -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 org.amnezia.vpn.shadowsocks.core.database.migration
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
|
||||
private val schema: String, private val keys: String) :
|
||||
Migration(oldVersion, newVersion) {
|
||||
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`")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.os.Build
|
||||
import org.amnezia.vpn.shadowsocks.core.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,42 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.net.LocalSocket
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||
|
||||
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,154 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.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 androidx.core.content.getSystemService
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.actor
|
||||
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()
|
||||
}
|
||||
@ObsoleteCoroutinesApi
|
||||
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(DefaultNetworkListener.NetworkMessage.Start(key, listener))
|
||||
suspend fun get() = if (fallback) @TargetApi(23) {
|
||||
connectivity.activeNetwork ?: throw UnknownHostException() // failed to listen, return current if available
|
||||
} else DefaultNetworkListener.NetworkMessage.Get().run {
|
||||
networkActor.send(this)
|
||||
response.await()
|
||||
}
|
||||
suspend fun stop(key: Any) = networkActor.send(DefaultNetworkListener.NetworkMessage.Stop(key))
|
||||
|
||||
|
||||
private object Callback: ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
// it's a good idea to refresh capabilities
|
||||
runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Update(network)) }
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
super.onLost(network)
|
||||
runBlocking {
|
||||
networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
||||
// private object Callback : ConnectivityManager.NetworkCallback() {
|
||||
// override fun onAvailable(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
|
||||
// override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
||||
// // it's a good idea to refresh capabilities
|
||||
// runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Update(network)) }
|
||||
// }
|
||||
// override fun onLost(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network)) }
|
||||
// }
|
||||
|
||||
private var fallback = false
|
||||
private val connectivity = app.getSystemService<ConnectivityManager>()!!
|
||||
private val request = NetworkRequest.Builder().apply {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
}.build()
|
||||
/**
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* 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) {
|
||||
connectivity.registerDefaultNetworkCallback(Callback)
|
||||
} else try {
|
||||
fallback = false
|
||||
// we want REQUEST here instead of LISTEN
|
||||
connectivity.requestNetwork(request, Callback)
|
||||
} catch (e: SecurityException) {
|
||||
fallback = true
|
||||
}
|
||||
}
|
||||
private fun unregister() = connectivity.unregisterNetworkCallback(Callback)
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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 org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.disconnectFromMain
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
/**
|
||||
* 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: Pair<HttpURLConnection, Job>? = null
|
||||
val status = MutableLiveData<Status>().apply { value = Status.Idle }
|
||||
|
||||
fun testConnection() {
|
||||
cancelTest()
|
||||
status.value = Status.Testing
|
||||
val url = URL("https", when ((Core.currentProfile ?: return).first.route) {
|
||||
Acl.CHINALIST -> "www.qualcomm.cn"
|
||||
else -> "www.google.com"
|
||||
}, "/generate_204")
|
||||
val conn = (if (DataStore.serviceMode != Key.modeVpn) {
|
||||
url.openConnection(Proxy(Proxy.Type.SOCKS, DataStore.proxyAddress))
|
||||
} else url.openConnection()) as HttpURLConnection
|
||||
conn.setRequestProperty("Connection", "close")
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.useCaches = false
|
||||
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
status.value = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
val code = conn.responseCode
|
||||
val elapsed = SystemClock.elapsedRealtime() - start
|
||||
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed)
|
||||
else Status.Error.UnexpectedResponseCode(code)
|
||||
} catch (e: IOException) {
|
||||
Status.Error.IOFailure(e)
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelTest() = running?.let { (conn, job) ->
|
||||
job.cancel() // ensure job is cancelled before interrupting
|
||||
conn.disconnectFromMain()
|
||||
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()
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.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) : 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 val monitor = ChannelMonitor()
|
||||
|
||||
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
|
||||
|
||||
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
|
||||
configureBlocking(false)
|
||||
socket().bind(listen)
|
||||
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
|
||||
printLog(e)
|
||||
return forward(packet)
|
||||
}
|
||||
return supervisorScope {
|
||||
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
|
||||
try {
|
||||
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
|
||||
val question = request.question
|
||||
if (question?.type != Type.A) return@supervisorScope remote.await()
|
||||
val host = question.name.toString(true)
|
||||
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
|
||||
val localResults = try {
|
||||
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
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()
|
||||
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
||||
header.setFlag(Flags.RA.toInt()) // recursion available
|
||||
for (address in localResults) addRecord(when (address) {
|
||||
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
|
||||
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
|
||||
else -> throw IllegalStateException("Unsupported address $address")
|
||||
}, Section.ANSWER)
|
||||
}.toWire())
|
||||
} else remote.await()
|
||||
} catch (e: Exception) {
|
||||
remote.cancel()
|
||||
when (e) {
|
||||
is CancellationException -> { } // ignore
|
||||
else -> printLog(e)
|
||||
}
|
||||
ByteBuffer.wrap(prepareDnsResponse(request).apply {
|
||||
header.rcode = Rcode.SERVFAIL
|
||||
}.toWire())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalUnsignedTypes
|
||||
private suspend fun forward(packet: ByteBuffer): ByteBuffer {
|
||||
packet.position(0) // the packet might have been parsed, reset to beginning
|
||||
return if (tcp) SocketChannel.open().use { channel ->
|
||||
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,79 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.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 org.amnezia.vpn.shadowsocks.core.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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.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 -> throw IllegalStateException("Unsupported address type")
|
||||
}
|
||||
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)
|
||||
@ExperimentalUnsignedTypes
|
||||
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
|
||||
suspend fun readBytes(till: Int) {
|
||||
if (buffer.position() >= till) return
|
||||
while (reader(buffer) >= 0 && buffer.position() < till) wait()
|
||||
if (buffer.position() < till) throw EOFException("${buffer.position()} < $till")
|
||||
}
|
||||
suspend fun read(index: Int): Byte {
|
||||
readBytes(index + 1)
|
||||
return buffer[index]
|
||||
}
|
||||
check(read(0) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
|
||||
if (read(1) != 0.toByte()) throw IOException("Unsupported authentication ${buffer[1]}")
|
||||
check(read(2) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
|
||||
if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}")
|
||||
val dataOffset = when (read(5)) {
|
||||
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
||||
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
|
||||
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
||||
else -> throw IllegalStateException("Unsupported address type ${buffer[5]}")
|
||||
} + 8
|
||||
readBytes(dataOffset + 2)
|
||||
buffer.limit(buffer.position()) // store old position to update mark
|
||||
buffer.position(dataOffset)
|
||||
val dataLength = buffer.short.toUShort().toInt()
|
||||
val end = buffer.position() + dataLength
|
||||
check(end <= buffer.capacity()) { "Buffer too small to contain the message" }
|
||||
buffer.mark()
|
||||
buffer.position(buffer.limit()) // restore old position
|
||||
buffer.limit(end)
|
||||
readBytes(buffer.limit())
|
||||
buffer.reset()
|
||||
}
|
||||
|
||||
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.position(3)
|
||||
packet.position(6 + when (packet.get()) {
|
||||
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
|
||||
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
|
||||
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
|
||||
else -> throw IllegalStateException("Unsupported address type")
|
||||
})
|
||||
packet.mark()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.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 {
|
||||
if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException("prefixSize: $prefixSize")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.net
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.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,33 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.os.Bundle
|
||||
|
||||
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
|
||||
init {
|
||||
check(resolveInfo.providerInfo != null)
|
||||
}
|
||||
|
||||
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
|
||||
override val packageName: String get() = resolveInfo.providerInfo.packageName
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
|
||||
object NoPlugin : Plugin() {
|
||||
override val id: String get() = ""
|
||||
override val label: CharSequence get() = app.getText(R.string.plugin_disabled)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
abstract class Plugin {
|
||||
abstract val id: String
|
||||
abstract val label: CharSequence
|
||||
open val icon: Drawable? get() = null
|
||||
open val defaultConfig: String? get() = null
|
||||
open val packageName: String get() = ""
|
||||
open val trusted: Boolean get() = true
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Commandline
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
|
||||
import java.util.*
|
||||
|
||||
class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val selected: String) {
|
||||
private constructor(plugins: List<PluginOptions>) : this(
|
||||
plugins.filter { it.id.isNotEmpty() }.associate { it.id to it },
|
||||
if (plugins.isEmpty()) "" else plugins[0].id)
|
||||
constructor(plugin: String) : this(plugin.split('\n').map { line ->
|
||||
if (line.startsWith("kcptun ")) {
|
||||
val opt = PluginOptions()
|
||||
opt.id = "kcptun"
|
||||
try {
|
||||
val iterator = Commandline.translateCommandline(line).drop(1).iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val option = iterator.next()
|
||||
when {
|
||||
option == "--nocomp" -> opt["nocomp"] = null
|
||||
option.startsWith("--") -> opt[option.substring(2)] = iterator.next()
|
||||
else -> throw IllegalArgumentException("Unknown kcptun parameter: $option")
|
||||
}
|
||||
}
|
||||
} catch (exc: Exception) {
|
||||
}
|
||||
opt
|
||||
} else PluginOptions(line)
|
||||
})
|
||||
|
||||
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else
|
||||
pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig)
|
||||
val selectedOptions: PluginOptions get() = getOptions(selected)
|
||||
|
||||
override fun toString(): String {
|
||||
val result = LinkedList<PluginOptions>()
|
||||
for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
|
||||
if (!pluginsOptions.contains(selected)) result.addFirst(selectedOptions)
|
||||
return result.joinToString("\n") { it.toString(false) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.Signature
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.system.Os
|
||||
import android.util.Base64
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.signaturesCompat
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginContract
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
object PluginManager {
|
||||
class PluginNotFoundException(private val plugin: String) : FileNotFoundException(plugin) {
|
||||
override fun getLocalizedMessage() = app.getString(R.string.plugin_unknown, plugin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
|
||||
* to publish or has published plugins for this app. You can obtain your public key by executing:
|
||||
*
|
||||
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
|
||||
*
|
||||
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
|
||||
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
|
||||
*/
|
||||
val trustedSignatures by lazy {
|
||||
Core.packageInfo.signaturesCompat.toSet() +
|
||||
Signature(Base64.decode( // @Mygod
|
||||
"""
|
||||
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|
||||
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|
||||
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|
||||
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|
||||
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|
||||
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|
||||
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|
||||
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|
||||
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|
||||
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|
||||
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|
||||
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|
||||
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|
||||
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|
||||
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|
||||
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|
||||
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
|
||||
""", Base64.DEFAULT)) +
|
||||
Signature(Base64.decode( // @madeye
|
||||
"""
|
||||
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|
||||
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|
||||
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|
||||
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|
||||
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|
||||
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|
||||
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|
||||
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|
||||
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|
||||
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|
||||
|1iNuLIssdhDz3IL9Yg==
|
||||
""", Base64.DEFAULT))
|
||||
}
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
private var cachedPlugins: Map<String, Plugin>? = null
|
||||
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
|
||||
if (receiver == null) receiver = Core.listenForPackageChanges {
|
||||
synchronized(this) {
|
||||
receiver = null
|
||||
cachedPlugins = null
|
||||
}
|
||||
}
|
||||
if (cachedPlugins == null) {
|
||||
val pm = app.packageManager
|
||||
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
|
||||
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
|
||||
}
|
||||
cachedPlugins!!
|
||||
}
|
||||
|
||||
private fun buildUri(id: String) = Uri.Builder()
|
||||
.scheme(PluginContract.SCHEME)
|
||||
.authority(PluginContract.AUTHORITY)
|
||||
.path("/$id")
|
||||
.build()
|
||||
fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
|
||||
|
||||
// the following parts are meant to be used by :bg
|
||||
@Throws(Throwable::class)
|
||||
fun init(options: PluginOptions): String? {
|
||||
if (options.id.isEmpty()) return null
|
||||
var throwable: Throwable? = null
|
||||
|
||||
try {
|
||||
val path = initNative(options)
|
||||
if (path != null) return path
|
||||
} catch (t: Throwable) {
|
||||
if (throwable == null) throwable = t else printLog(t)
|
||||
}
|
||||
|
||||
// add other plugin types here
|
||||
|
||||
throw throwable ?: PluginNotFoundException(options.id)
|
||||
}
|
||||
|
||||
private fun initNative(options: PluginOptions): String? {
|
||||
val providers = app.packageManager.queryIntentContentProviders(
|
||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
|
||||
if (providers.isEmpty()) return null
|
||||
val uri = Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(providers.single().providerInfo.authority)
|
||||
.build()
|
||||
val cr = app.contentResolver
|
||||
return try {
|
||||
initNativeFast(cr, options, uri)
|
||||
} catch (t: Throwable) {
|
||||
printLog(t)
|
||||
initNativeSlow(cr, options, uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String {
|
||||
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null,
|
||||
bundleOf(Pair(PluginContract.EXTRA_OPTIONS, options.id)))!!.getString(PluginContract.EXTRA_ENTRY)!!
|
||||
check(File(result).canExecute())
|
||||
return result
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
|
||||
var initialized = false
|
||||
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
|
||||
val pluginDir = File(Core.deviceStorage.noBackupFilesDir, "plugin")
|
||||
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
|
||||
?: return null).use { cursor ->
|
||||
if (!cursor.moveToFirst()) entryNotFound()
|
||||
pluginDir.deleteRecursively()
|
||||
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
|
||||
val pluginDirPath = pluginDir.absolutePath + '/'
|
||||
do {
|
||||
val path = cursor.getString(0)
|
||||
val file = File(pluginDir, path)
|
||||
check(file.absolutePath.startsWith(pluginDirPath))
|
||||
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
|
||||
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
|
||||
}
|
||||
Os.chmod(file.absolutePath, when (cursor.getType(1)) {
|
||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
|
||||
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
|
||||
else -> throw IllegalArgumentException("File mode should be of type int")
|
||||
})
|
||||
if (path == options.id) initialized = true
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
if (!initialized) entryNotFound()
|
||||
return File(pluginDir, options.id).absolutePath
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.plugin
|
||||
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.signaturesCompat
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginContract
|
||||
|
||||
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
|
||||
protected abstract val metaData: Bundle
|
||||
|
||||
override val id: String by lazy { metaData.getString(PluginContract.METADATA_KEY_ID)!! }
|
||||
override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) }
|
||||
override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) }
|
||||
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG)!! }
|
||||
override val packageName: String get() = resolveInfo.resolvePackageName
|
||||
override val trusted by lazy {
|
||||
Core.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.preference
|
||||
|
||||
import android.os.Binder
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.database.PrivateDatabase
|
||||
import org.amnezia.vpn.shadowsocks.core.database.PublicDatabase
|
||||
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
|
||||
object DataStore : OnPreferenceDataStoreChangeListener {
|
||||
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
|
||||
// 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, true)
|
||||
val serviceMode get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
|
||||
|
||||
/**
|
||||
* An alternative way to detect this interface could be checking MAC address = 00:ff:aa:00:00:55, but there is no
|
||||
* reliable way of getting MAC address for now.
|
||||
*/
|
||||
val hasArc0 by lazy {
|
||||
var retry = 0
|
||||
while (retry < 5) {
|
||||
try {
|
||||
return@lazy NetworkInterface.getByName("arc0") != null
|
||||
} catch (_: SocketException) { }
|
||||
retry++
|
||||
Thread.sleep(100L shl retry)
|
||||
}
|
||||
false
|
||||
}
|
||||
/**
|
||||
* Binding bogus IP address 100.115.92.2 in Chrome OS directly does not seem to work reliably. It might be due to
|
||||
* the IP may not be available when the device is not connected to any network.
|
||||
*/
|
||||
val listenAddress get() = if (publicStore.getBoolean(Key.shareOverLan, hasArc0)) "0.0.0.0" else "127.0.0.1"
|
||||
var portProxy: Int
|
||||
get() = getLocalPort(Key.portProxy, 1080)
|
||||
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
||||
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())
|
||||
var portTransproxy: Int
|
||||
get() = getLocalPort(Key.portTransproxy, 8200)
|
||||
set(value) = publicStore.putString(Key.portTransproxy, 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
|
||||
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||
}
|
||||
|
||||
var editingId: Long?
|
||||
get() = privateStore.getLong(Key.id)
|
||||
set(value) = privateStore.putLong(Key.id, value)
|
||||
var proxyApps: Boolean
|
||||
get() = privateStore.getBoolean(Key.proxyApps) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.proxyApps, value)
|
||||
var bypass: Boolean
|
||||
get() = privateStore.getBoolean(Key.bypass) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.bypass, value)
|
||||
var individual: String
|
||||
get() = privateStore.getString(Key.individual) ?: ""
|
||||
set(value) = privateStore.putString(Key.individual, value)
|
||||
var plugin: String
|
||||
get() = privateStore.getString(Key.plugin) ?: ""
|
||||
set(value) = privateStore.putString(Key.plugin, value)
|
||||
var udpFallback: Long?
|
||||
get() = privateStore.getLong(Key.udpFallback)
|
||||
set(value) = privateStore.putLong(Key.udpFallback, value)
|
||||
var dirty: Boolean
|
||||
get() = privateStore.getBoolean(Key.dirty) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.dirty, value)
|
||||
}
|
||||
|
|
@ -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 org.amnezia.vpn.shadowsocks.core.preference
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
interface OnPreferenceDataStoreChangeListener {
|
||||
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?)
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.preference
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.database.KeyValuePair
|
||||
import java.util.HashSet
|
||||
|
||||
@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,51 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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 org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import org.json.JSONArray
|
||||
|
||||
private sealed class ArrayIterator<out T> : Iterator<T> {
|
||||
abstract val size: Int
|
||||
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 JSONArrayIterator(private val arr: JSONArray) : ArrayIterator<Any>() {
|
||||
override val size get() = arr.length()
|
||||
override fun get(index: Int) = arr.get(index)
|
||||
}
|
||||
fun JSONArray.asIterable() = Iterable { JSONArrayIterator(this) }
|
||||
|
||||
private class SortedListIterator<out T>(private val list: SortedList<T>) : ArrayIterator<T>() {
|
||||
override val size get() = list.size()
|
||||
override fun get(index: Int) = list[index]
|
||||
}
|
||||
fun <T> SortedList<T>.asIterable() = Iterable { SortedListIterator(this) }
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Commandline objects help handling command lines specifying processes to
|
||||
* execute.
|
||||
*
|
||||
* The class can be used to define a command line as nested elements or as a
|
||||
* helper to define a command line by an application.
|
||||
*
|
||||
*
|
||||
* `
|
||||
* <someelement><br></br>
|
||||
* <acommandline executable="/executable/to/run"><br></br>
|
||||
* <argument value="argument 1" /><br></br>
|
||||
* <argument line="argument_1 argument_2 argument_3" /><br></br>
|
||||
* <argument value="argument 4" /><br></br>
|
||||
* </acommandline><br></br>
|
||||
* </someelement><br></br>
|
||||
` *
|
||||
*
|
||||
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
|
||||
*
|
||||
* Adds support for escape character '\'.
|
||||
*/
|
||||
object Commandline {
|
||||
|
||||
/**
|
||||
* Quote the parts of the given array in way that makes them
|
||||
* usable as command line arguments.
|
||||
* @param args the list of arguments to quote.
|
||||
* @return empty string for null or no command, else every argument split
|
||||
* by spaces and quoted by quoting rules.
|
||||
*/
|
||||
fun toString(args: Iterable<String>?): String {
|
||||
// empty path return empty string
|
||||
if (args == null) {
|
||||
return ""
|
||||
}
|
||||
// path containing one or more elements
|
||||
val result = StringBuilder()
|
||||
for (arg in args) {
|
||||
if (result.isNotEmpty()) result.append(' ')
|
||||
(0 until arg.length)
|
||||
.map { arg[it] }
|
||||
.forEach {
|
||||
when (it) {
|
||||
' ', '\\', '"', '\'' -> {
|
||||
result.append('\\') // intentionally no break
|
||||
result.append(it)
|
||||
}
|
||||
else -> result.append(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote the parts of the given array in way that makes them
|
||||
* usable as command line arguments.
|
||||
* @param args the list of arguments to quote.
|
||||
* @return empty string for null or no command, else every argument split
|
||||
* by spaces and quoted by quoting rules.
|
||||
*/
|
||||
fun toString(args: Array<String>) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable
|
||||
|
||||
/**
|
||||
* Crack a command line.
|
||||
* @param toProcess the command line to process.
|
||||
* @return the command line broken into strings.
|
||||
* An empty or null toProcess parameter results in a zero sized array.
|
||||
*/
|
||||
fun translateCommandline(toProcess: String?): Array<String> {
|
||||
if (toProcess == null || toProcess.isEmpty()) {
|
||||
//no command? no string
|
||||
return arrayOf()
|
||||
}
|
||||
// parse with a simple finite state machine
|
||||
|
||||
val normal = 0
|
||||
val inQuote = 1
|
||||
val inDoubleQuote = 2
|
||||
var state = normal
|
||||
val tok = StringTokenizer(toProcess, "\\\"\' ", true)
|
||||
val result = ArrayList<String>()
|
||||
val current = StringBuilder()
|
||||
var lastTokenHasBeenQuoted = false
|
||||
var lastTokenIsSlash = false
|
||||
|
||||
while (tok.hasMoreTokens()) {
|
||||
val nextTok = tok.nextToken()
|
||||
when (state) {
|
||||
inQuote -> if ("\'" == nextTok) {
|
||||
lastTokenHasBeenQuoted = true
|
||||
state = normal
|
||||
} else {
|
||||
current.append(nextTok)
|
||||
}
|
||||
inDoubleQuote -> if ("\"" == nextTok) {
|
||||
if (lastTokenIsSlash) {
|
||||
current.append(nextTok)
|
||||
lastTokenIsSlash = false
|
||||
} else {
|
||||
lastTokenHasBeenQuoted = true
|
||||
state = normal
|
||||
}
|
||||
} else if ("\\" == nextTok) {
|
||||
lastTokenIsSlash = if (lastTokenIsSlash) {
|
||||
current.append(nextTok)
|
||||
false
|
||||
} else
|
||||
true
|
||||
} else {
|
||||
if (lastTokenIsSlash) {
|
||||
current.append("\\") // unescaped
|
||||
lastTokenIsSlash = false
|
||||
}
|
||||
current.append(nextTok)
|
||||
}
|
||||
else -> {
|
||||
if (lastTokenIsSlash) {
|
||||
current.append(nextTok)
|
||||
lastTokenIsSlash = false
|
||||
} else if ("\\" == nextTok)
|
||||
lastTokenIsSlash = true
|
||||
else if ("\'" == nextTok) {
|
||||
state = inQuote
|
||||
} else if ("\"" == nextTok) {
|
||||
state = inDoubleQuote
|
||||
} else if (" " == nextTok) {
|
||||
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
|
||||
result.add(current.toString())
|
||||
current.setLength(0)
|
||||
}
|
||||
} else {
|
||||
current.append(nextTok)
|
||||
}
|
||||
lastTokenHasBeenQuoted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
|
||||
result.add(current.toString())
|
||||
}
|
||||
if (state == inQuote || state == inDoubleQuote) {
|
||||
throw IllegalArgumentException("unbalanced quotes in $toProcess")
|
||||
}
|
||||
if (lastTokenIsSlash) throw IllegalArgumentException("escape character following nothing in $toProcess")
|
||||
return result.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
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 individual = "Proxyed"
|
||||
|
||||
const val serviceMode = "serviceMode"
|
||||
const val modeProxy = "proxy"
|
||||
const val modeVpn = "vpn"
|
||||
const val modeTransproxy = "transproxy"
|
||||
const val shareOverLan = "shareOverLan"
|
||||
const val portProxy = "portProxy"
|
||||
const val portLocalDns = "portLocalDns"
|
||||
const val portTransproxy = "portTransproxy"
|
||||
|
||||
const val route = "route"
|
||||
|
||||
const val isAutoConnect = "isAutoConnect"
|
||||
const val directBootAware = "directBootAware"
|
||||
|
||||
const val proxyApps = "isProxyApps"
|
||||
const val bypass = "isBypassApps"
|
||||
const val udpdns = "isUdpDns"
|
||||
const val ipv6 = "isIpv6"
|
||||
const val metered = "metered"
|
||||
|
||||
const val host = "proxy"
|
||||
const val password = "sitekey"
|
||||
const val method = "encMethod"
|
||||
const val remotePort = "remotePortNum"
|
||||
const val remoteDns = "remoteDns"
|
||||
|
||||
const val plugin = "plugin"
|
||||
const val pluginConfigure = "plugin.configure"
|
||||
const val udpFallback = "udpFallback"
|
||||
|
||||
const val dirty = "profileDirty"
|
||||
|
||||
const val tfo = "tcp_fastopen"
|
||||
const val assetUpdateTime = "assetUpdateTime"
|
||||
|
||||
// TV specific values
|
||||
const val controlStats = "control.stats"
|
||||
const val controlImport = "control.import"
|
||||
const val controlExport = "control.export"
|
||||
const val about = "about"
|
||||
}
|
||||
|
||||
object Action {
|
||||
const val SERVICE = "org.amnezia.vpn.shadowsocks.SERVICE"
|
||||
const val CLOSE = "org.amnezia.vpn.shadowsocks.CLOSE"
|
||||
const val RELOAD = "org.amnezia.vpn.shadowsocks.RELOAD"
|
||||
|
||||
const val EXTRA_PROFILE_ID = "org.amnezia.vpn.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 org.amnezia.vpn.shadowsocks.core.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,59 @@
|
|||
package org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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 org.amnezia.vpn.shadowsocks.core.utils
|
||||
|
||||
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 java.net.HttpURLConnection
|
||||
import java.net.InetAddress
|
||||
|
||||
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
|
||||
|
||||
private val parseNumericAddress by lazy {
|
||||
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
||||
isAccessible = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A slightly more performant variant of InetAddress.parseNumericAddress.
|
||||
*
|
||||
* Bug: https://issuetracker.google.com/issues/123456213
|
||||
*/
|
||||
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
|
||||
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
|
||||
|
||||
fun HttpURLConnection.disconnectFromMain() {
|
||||
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
|
||||
}
|
||||
|
||||
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
||||
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) {
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
fun Preference.remove() = parent!!.removePreference(this)
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.plugin
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
/**
|
||||
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
|
||||
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
|
||||
* class, implement the abstract methods, and add it to your manifest like this:
|
||||
*
|
||||
* <pre class="prettyprint"><manifest>
|
||||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <provider android:name="org.amnezia.vpn.shadowsocks.$PLUGIN_ID.BinaryProvider"
|
||||
* android:authorities="org.amnezia.vpn.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
|
||||
* <intent-filter>
|
||||
* <category android:name="org.amnezia.vpn.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||
* </intent-filter>
|
||||
* </provider>
|
||||
* ...
|
||||
* </application>
|
||||
*</manifest></pre>
|
||||
*/
|
||||
abstract class NativePluginProvider : ContentProvider() {
|
||||
override fun getType(p0: Uri): String = "application/x-elf"
|
||||
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
/**
|
||||
* Provide all files needed for native plugin.
|
||||
*
|
||||
* @param provider A helper object to use to add files.
|
||||
*/
|
||||
protected abstract fun populateFiles(provider: PathProvider)
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
|
||||
sortOrder: String?): Cursor {
|
||||
check(selection == null && selectionArgs == null && sortOrder == null)
|
||||
val result = MatrixCursor(projection)
|
||||
populateFiles(PathProvider(uri, result))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
|
||||
*
|
||||
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
|
||||
* default behavior.
|
||||
*
|
||||
* @return Absolute path for executable entry.
|
||||
*/
|
||||
open fun getExecutable(): String = throw UnsupportedOperationException()
|
||||
|
||||
abstract fun openFile(uri: Uri?): ParcelFileDescriptor
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||
check(mode == "r")
|
||||
return openFile(uri)
|
||||
}
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
|
||||
PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))
|
||||
else -> super.call(method, arg, extras)
|
||||
}
|
||||
|
||||
// Methods that should not be used
|
||||
override fun insert(p0: Uri, p1: ContentValues?): Uri = throw UnsupportedOperationException()
|
||||
override fun update(p0: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =
|
||||
throw UnsupportedOperationException()
|
||||
override fun delete(uri: Uri, p1: String?, p2: Array<out String>?): Int = throw UnsupportedOperationException()
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.plugin
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Helper class to provide relative paths of files to copy.
|
||||
*/
|
||||
class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
|
||||
private val basePath = baseUri.path?.trim('/') ?: ""
|
||||
|
||||
fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
|
||||
val trimmed = path.trim('/')
|
||||
if (trimmed.startsWith(basePath)) cursor.newRow()
|
||||
.add(PluginContract.COLUMN_PATH, trimmed)
|
||||
.add(PluginContract.COLUMN_MODE, mode)
|
||||
return this
|
||||
}
|
||||
fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
|
||||
var sub = to + file.name
|
||||
if (basePath.startsWith(sub)) if (file.isDirectory) {
|
||||
sub += '/'
|
||||
file.listFiles().forEach { addTo(it, sub, mode) }
|
||||
} else addPath(sub, mode)
|
||||
return this
|
||||
}
|
||||
fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
|
||||
if (basePath.startsWith(at))
|
||||
if (file.isDirectory) file.listFiles().forEach { addTo(it, at, mode) } else addPath(at, mode)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.plugin
|
||||
|
||||
/**
|
||||
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
|
||||
*
|
||||
* This class is written in Java to keep Java interoperability.
|
||||
*/
|
||||
object PluginContract {
|
||||
/**
|
||||
* ContentProvider Action: Used for NativePluginProvider.
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
*/
|
||||
const val ACTION_NATIVE_PLUGIN = "org.amnezia.vpn.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
|
||||
/**
|
||||
* Activity Action: Used for ConfigurationActivity.
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
*/
|
||||
const val ACTION_CONFIGURE = "org.amnezia.vpn.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
/**
|
||||
* Activity Action: Used for HelpActivity or HelpCallback.
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.ACTION_HELP"
|
||||
*/
|
||||
const val ACTION_HELP = "org.amnezia.vpn.shadowsocks.plugin.ACTION_HELP"
|
||||
|
||||
/**
|
||||
* The lookup key for a string that provides the plugin entry binary.
|
||||
*
|
||||
* Example: "/data/data/org.amnezia.vpn.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
*/
|
||||
const val EXTRA_ENTRY = "org.amnezia.vpn.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
/**
|
||||
* The lookup key for a string that provides the options as a string.
|
||||
*
|
||||
* Example: "obfs=http;obfs-host=www.baidu.com"
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
*/
|
||||
const val EXTRA_OPTIONS = "org.amnezia.vpn.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
/**
|
||||
* The lookup key for a CharSequence that provides user relevant help message.
|
||||
*
|
||||
* Example: "obfs=<http></http>|tls> Enable obfuscating: HTTP or TLS (Experimental).
|
||||
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
</host_name> */
|
||||
const val EXTRA_HELP_MESSAGE = "org.amnezia.vpn.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
|
||||
/**
|
||||
* The metadata key to retrieve plugin id. Required for plugins.
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.id"
|
||||
*/
|
||||
const val METADATA_KEY_ID = "org.amnezia.vpn.shadowsocks.plugin.id"
|
||||
/**
|
||||
* The metadata key to retrieve default configuration. Default value is empty.
|
||||
*
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.default_config"
|
||||
*/
|
||||
const val METADATA_KEY_DEFAULT_CONFIG = "org.amnezia.vpn.shadowsocks.plugin.default_config"
|
||||
|
||||
const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
|
||||
|
||||
/** ConfigurationActivity result: fallback to manual edit mode. */
|
||||
const val RESULT_FALLBACK = 1
|
||||
|
||||
/**
|
||||
* Relative to the file to be copied. This column is required.
|
||||
*
|
||||
* Example: "kcptun", "doc/help.txt"
|
||||
*
|
||||
* Type: String
|
||||
*/
|
||||
const val COLUMN_PATH = "path"
|
||||
/**
|
||||
* File mode bits. Default value is "644".
|
||||
*
|
||||
* Example: "755"
|
||||
*
|
||||
* Type: String
|
||||
*/
|
||||
const val COLUMN_MODE = "mode"
|
||||
|
||||
/**
|
||||
* The scheme for general plugin actions.
|
||||
*/
|
||||
const val SCHEME = "plugin"
|
||||
/**
|
||||
* The authority for general plugin actions.
|
||||
*/
|
||||
const val AUTHORITY = "org.amnezia.vpn.shadowsocks"
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.plugin
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Helper class for processing plugin options.
|
||||
*
|
||||
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
|
||||
*/
|
||||
class PluginOptions : HashMap<String, String?> {
|
||||
var id = ""
|
||||
|
||||
constructor() : super()
|
||||
constructor(initialCapacity: Int) : super(initialCapacity)
|
||||
constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
|
||||
|
||||
private constructor(options: String?, parseId: Boolean) : this() {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
var parseId = parseId
|
||||
if (options.isNullOrEmpty()) return
|
||||
check(options.all { !it.isISOControl() }) { "No control characters allowed." }
|
||||
val tokenizer = StringTokenizer("$options;", "\\=;", true)
|
||||
val current = StringBuilder()
|
||||
var key: String? = null
|
||||
while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
|
||||
"\\" -> current.append(tokenizer.nextToken())
|
||||
"=" -> if (key == null) {
|
||||
key = current.toString()
|
||||
current.setLength(0)
|
||||
} else current.append(nextToken)
|
||||
";" -> {
|
||||
if (key != null) {
|
||||
put(key, current.toString())
|
||||
key = null
|
||||
} else if (current.isNotEmpty())
|
||||
if (parseId) id = current.toString() else put(current.toString(), null)
|
||||
current.setLength(0)
|
||||
parseId = false
|
||||
}
|
||||
else -> current.append(nextToken)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(options: String?) : this(options, true)
|
||||
constructor(id: String, options: String?) : this(options, false) {
|
||||
this.id = id
|
||||
}
|
||||
|
||||
/**
|
||||
* Put but if value is null or default, the entry is deleted.
|
||||
*
|
||||
* @return Old value before put.
|
||||
*/
|
||||
fun putWithDefault(key: String, value: String?, default: String? = null) =
|
||||
if (value == null || value == default) remove(key) else put(key, value)
|
||||
|
||||
private fun append(result: StringBuilder, str: String) = (0 until str.length)
|
||||
.map { str[it] }
|
||||
.forEach {
|
||||
when (it) {
|
||||
'\\', '=', ';' -> {
|
||||
result.append('\\') // intentionally no break
|
||||
result.append(it)
|
||||
}
|
||||
else -> result.append(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun toString(trimId: Boolean): String {
|
||||
val result = StringBuilder()
|
||||
if (!trimId) if (id.isEmpty()) return "" else append(result, id)
|
||||
for ((key, value) in entries) {
|
||||
if (result.isNotEmpty()) result.append(';')
|
||||
append(result, key)
|
||||
if (value != null) {
|
||||
result.append('=')
|
||||
append(result, value)
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
override fun toString(): String = toString(true)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
|
||||
}
|
||||
override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue