From 9e49266d5cccf247a149df84c20d20644f40c97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 2 Aug 2021 13:28:53 +0800 Subject: [PATCH] Add hysteria --- .github/workflows/debug.yml | 23 ++ .github/workflows/release_hysteria.yml | 194 +++++++++ .idea/misc.xml | 3 + .idea/vcs.xml | 1 + README.md | 17 +- .../3.json | 385 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 3 + .../java/io/nekohasekai/sagernet/Constants.kt | 4 + .../nekohasekai/sagernet/bg/V2RayInstance.kt | 32 +- .../sagernet/database/DataStore.kt | 4 + .../sagernet/database/ProxyEntity.kt | 132 +++--- .../sagernet/database/SagerDatabase.kt | 2 +- .../sagernet/fmt/KryoConverters.java | 7 + .../io/nekohasekai/sagernet/fmt/TypeMap.kt | 1 + .../sagernet/fmt/hysteria/HysteriaBean.java | 115 ++++++ .../sagernet/fmt/hysteria/HysteriaFmt.kt | 64 +++ .../fmt/shadowsocks/ShadowsocksFmt.kt | 6 +- .../sagernet/group/GroupUpdater.kt | 4 + .../nekohasekai/sagernet/group/RawUpdater.kt | 4 + .../sagernet/tun/DirectTunThread.kt | 4 + .../sagernet/ui/ConfigurationFragment.kt | 28 +- .../ui/profile/HysteriaSettingsActivity.kt | 95 +++++ .../res/drawable/ic_baseline_download_24.xml | 10 + app/src/main/res/menu/add_profile_menu.xml | 4 + app/src/main/res/values-zh-rCN/strings.xml | 5 + app/src/main/res/values/arrays.xml | 6 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/hysteria_preferences.xml | 62 +++ sager.properties | 4 +- 29 files changed, 1128 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/release_hysteria.yml create mode 100644 app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json create mode 100644 app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java create mode 100644 app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt create mode 100644 app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt create mode 100644 app/src/main/res/drawable/ic_baseline_download_24.xml create mode 100644 app/src/main/res/xml/hysteria_preferences.xml diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml index 95d88f54..e8bb29eb 100644 --- a/.github/workflows/debug.yml +++ b/.github/workflows/debug.yml @@ -161,6 +161,29 @@ jobs: - name: Native Build if: steps.cache.outputs.cache-hit != 'true' run: ./run plugin brook + hysteria: + name: Native Build (Hysteria) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Fetch Status + run: git submodule status 'plugin/hysteria/*' > hysteria_status + - name: Hysteria Cache + id: cache + uses: actions/cache@v2 + with: + path: | + plugin/hysteria/src/main/jniLibs + key: ${{ hashFiles('bin/lib/hysteria/*', 'hysteria_status') }} + - name: Install Golang + uses: actions/setup-go@v2 + if: steps.cache.outputs.cache-hit != 'true' + with: + go-version: 1.16 + - name: Native Build + if: steps.cache.outputs.cache-hit != 'true' + run: ./run plugin hysteria shadowsocks: name: Native Build (Shadowsocks) runs-on: ubuntu-latest diff --git a/.github/workflows/release_hysteria.yml b/.github/workflows/release_hysteria.yml new file mode 100644 index 00000000..cab2a826 --- /dev/null +++ b/.github/workflows/release_hysteria.yml @@ -0,0 +1,194 @@ +name: Hysteria Plugin Release Build +on: + workflow_dispatch: + inputs: + tag: + description: 'Release Tag' + required: true + upload: + description: 'Upload: If want ignore' + required: false + publish: + description: 'Publish: If want ignore' + required: false + play: + description: 'Play: If want ignore' + required: false +jobs: + check: + name: Check Access + runs-on: ubuntu-latest + steps: + - name: "Check access" + uses: "lannonbr/repo-permission-check-action@2.0.0" + with: + permission: "write" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + native: + name: Native Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Fetch Status + run: git submodule status 'plugin/hysteria/*' > hysteria_status + - name: Hysteria Cache + id: cache + uses: actions/cache@v2 + with: + path: | + plugin/hysteria/src/main/jniLibs + key: ${{ hashFiles('bin/lib/hysteria/*', 'hysteria_status') }} + - name: Gradle cache + uses: actions/cache@v2 + with: + path: ~/.gradle + key: gradle-${{ hashFiles('**/*.gradle') }} + - name: Install Golang + uses: actions/setup-go@v2 + if: steps.cache.outputs.cache-hit != 'true' + with: + go-version: 1.16 + - name: Native Build + if: steps.cache.outputs.cache-hit != 'true' + run: ./run plugin hysteria + build: + name: Gradle Build + runs-on: ubuntu-latest + needs: + - native + - check + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Fetch Status + run: git submodule status 'plugin/hysteria/*' > hysteria_status + - name: Hysteria Cache + uses: actions/cache@v2 + with: + path: | + plugin/hysteria/src/main/jniLibs + key: ${{ hashFiles('bin/lib/brook/*', 'hysteria_status') }} + - name: Gradle cache + uses: actions/cache@v2 + with: + path: ~/.gradle + key: gradle-${{ hashFiles('**/*.gradle') }} + - name: Release Build + env: + SKIP_BUILD: on + run: | + echo "sdk.dir=${ANDROID_HOME}" > local.properties + echo "ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529" >> local.properties + export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" + ./run init action library + ./gradlew :plugin:hysteria:assembleOssRelease + APK=$(find plugin/hysteria/build/outputs/apk -name '*arm64-v8a*.apk') + APK=$(dirname $APK) + echo "APK=$APK" >> $GITHUB_ENV + - uses: actions/upload-artifact@v2 + with: + name: APKs + path: ${{ env.APK }} + - uses: actions/upload-artifact@v2 + with: + name: "SHA256-ARM ${{ env.SHA256_ARM }}" + path: ${{ env.SUM_ARM }} + - uses: actions/upload-artifact@v2 + with: + name: "SHA256-ARM64 ${{ env.SHA256_ARM64 }}" + path: ${{ env.SUM_ARM64 }} + - uses: actions/upload-artifact@v2 + with: + name: "SHA256-X64 ${{ env.SHA256_X64 }}" + path: ${{ env.SUM_X64 }} + - uses: actions/upload-artifact@v2 + with: + name: "SHA256-X86 ${{ env.SHA256_X86 }}" + path: ${{ env.SUM_X86 }} + publish: + name: Publish Release + if: github.event.inputs.publish != 'y' + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Donwload Artifacts + uses: actions/download-artifact@v2 + with: + name: APKs + path: artifacts + - name: Release + run: | + wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz + tar -xvf ghr.tar.gz + mv ghr*linux_amd64/ghr . + mkdir apks + find artifacts -name "*.apk" -exec cp {} apks \; + find artifacts -name "*.sha256sum.txt" -exec cp {} apks \; + ./ghr -delete -prerelease -t "${{ github.token }}" -n "${{ github.event.inputs.tag }}" "${{ github.event.inputs.tag }}" apks + upload: + name: Upload Release + if: github.event.inputs.upload != 'y' + runs-on: ubuntu-latest + needs: build + steps: + - name: Donwload Artifacts + uses: actions/download-artifact@v2 + with: + name: APKs + path: artifacts + - name: Release + run: | + mkdir apks + find artifacts -name "*.apk" -exec cp {} apks \; + + function upload() { + for apk in $@; do + echo ">> Uploading $apk" + curl https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendDocument \ + -X POST \ + -F chat_id="${{ secrets.TELEGRAM_CHANNEL }}" \ + -F document="@$apk" \ + --silent --show-error --fail >/dev/null & + done + for job in $(jobs -p); do + wait $job || exit 1 + done + } + upload apks/* + play: + name: Publish to Play Store + if: github.event.inputs.upload != 'y' + runs-on: ubuntu-latest + needs: + - native + - check + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Fetch Status + run: git submodule status 'plugin/hysteria/*' > hysteria_status + - name: Hysteria Cache + uses: actions/cache@v2 + with: + path: | + plugin/hysteria/src/main/jniLibs + key: ${{ hashFiles('bin/lib/brook/*', 'hysteria_status') }} + - name: Gradle cache + uses: actions/cache@v2 + with: + path: ~/.gradle + key: gradle-${{ hashFiles('**/*.gradle') }} + - name: Release Build + run: | + echo "sdk.dir=${ANDROID_HOME}" > local.properties + echo "ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529" >> local.properties + export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" + cat > service_account_credentials.json << EOF + ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}" + EOF + ./run init action library + ./gradlew :plugin:hysteria:publishPlayReleaseBundle \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index d0f14e21..72da0eaf 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,6 +6,7 @@ + @@ -18,6 +19,7 @@ + @@ -74,6 +76,7 @@ + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94346c27..9db3fadb 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -23,6 +23,7 @@ + diff --git a/README.md b/README.md index bf0210c6..92928738 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,9 @@ The application is designed to be used whenever possible. #### Subscription -* Universal base64 format -* Shadowsocks SIP008 -* Just My Socks' proprietary format -* Clash +* Raw: All widely used formats (base64, clash or origin configuration) +* [Open Online Config](https://github.com/Shadowsocks-NET/OpenOnlineConfig) +* [Shadowsocks SIP008](https://shadowsocks.org/en/wiki/SIP008-Online-Configuration-Delivery.html) #### Features @@ -55,13 +54,19 @@ The application is designed to be used whenever possible. ## Localization -Is SagerNet not in your language, or the translation is incorrect or incomplete? Get involved in the translations on our [Weblate](https://hosted.weblate.org/engage/sagernet/). +Is SagerNet not in your language, or the translation is incorrect or incomplete? Get involved in the +translations on our [Weblate](https://hosted.weblate.org/engage/sagernet/). [![Translation status](https://hosted.weblate.org/widgets/sagernet/-/horizontal-auto.svg)](https://hosted.weblate.org/engage/sagernet/) ### Adding a new language -First and foremost, Android must already support the specific language and locale you want to add. We cannot work with languages that Android and the SDK do not support, the tools simply break down. Next, if you are considering adding a country-specific variant of a language (e.g. de-AT), first make sure that the main language is well maintained (e.g. de). Your contribution might be useful to more people if you contribute to the existing version of your language rather than the country-specific variant. +First and foremost, Android must already support the specific language and locale you want to add. +We cannot work with languages that Android and the SDK do not support, the tools simply break down. +Next, if you are considering adding a country-specific variant of a language (e.g. de-AT), first +make sure that the main language is well maintained (e.g. de). Your contribution might be useful to +more people if you contribute to the existing version of your language rather than the +country-specific variant. Anyone can create a new language via Weblate. diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json new file mode 100644 index 00000000..230d2199 --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json @@ -0,0 +1,385 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "3e2a0dd9879b5afd0ca3e9d55c7e0347", + "entities": [ + { + "tableName": "proxy_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ungrouped", + "columnName": "ungrouped", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription", + "columnName": "subscription", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "proxy_entities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tx", + "columnName": "tx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rx", + "columnName": "rx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ping", + "columnName": "ping", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "socksBean", + "columnName": "socksBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "httpBean", + "columnName": "httpBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "ssBean", + "columnName": "ssBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "ssrBean", + "columnName": "ssrBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "vlessBean", + "columnName": "vlessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "ptBean", + "columnName": "ptBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "rbBean", + "columnName": "rbBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "brookBean", + "columnName": "brookBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "balancerBean", + "columnName": "balancerBean", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domains", + "columnName": "domains", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ip", + "columnName": "ip", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourcePort", + "columnName": "sourcePort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "network", + "columnName": "network", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attrs", + "columnName": "attrs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reverse", + "columnName": "reverse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "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, '3e2a0dd9879b5afd0ca3e9d55c7e0347')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c1a4791c..1c3aeed3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -167,6 +167,9 @@ + diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index 6225d95b..c0fe2a72 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -142,6 +142,10 @@ object Key { const val SERVER_HEADERS = "serverHeaders" const val SERVER_ALLOW_INSECURE = "serverAllowInsecure" + const val SERVER_AUTH_TYPE = "serverAuthType" + const val SERVER_UPLOAD_SPEED = "serverUploadSpeed" + const val SERVER_DOWNLOAD_SPEED = "serverDownloadSpeed" + const val BALANCER_TYPE = "balancerType" const val BALANCER_GROUP = "balancerGroup" const val BALANCER_STRATEGY = "balancerStrategy" diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/V2RayInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/V2RayInstance.kt index e680e7d2..2ee4ab5b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/V2RayInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/V2RayInstance.kt @@ -37,6 +37,8 @@ import io.nekohasekai.sagernet.fmt.V2rayBuildResult import io.nekohasekai.sagernet.fmt.brook.BrookBean import io.nekohasekai.sagernet.fmt.brook.internalUri import io.nekohasekai.sagernet.fmt.buildV2RayConfig +import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean +import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig import io.nekohasekai.sagernet.fmt.internal.ConfigBean import io.nekohasekai.sagernet.fmt.naive.NaiveBean import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig @@ -145,6 +147,10 @@ open class V2RayInstance(val profile: ProxyEntity) { bean is BrookBean -> { initPlugin("brook-plugin") } + bean is HysteriaBean -> { + initPlugin("hysteria-plugin") + pluginConfigs[port] = profile.type to bean.buildHysteriaConfig(port) + } bean is ConfigBean -> { when (bean.type) { "trojan-go" -> { @@ -154,7 +160,9 @@ open class V2RayInstance(val profile: ProxyEntity) { ) } else -> { - externalInstances[port] = ExternalInstance(v2rayPoint.supportSet, profile, port).apply { + externalInstances[port] = ExternalInstance( + v2rayPoint.supportSet, profile, port + ).apply { init() } } @@ -354,6 +362,28 @@ open class V2RayInstance(val profile: ProxyEntity) { processes.start(commands) } + bean is HysteriaBean -> { + val configFile = File( + context.noBackupFilesDir, + "hysteria_" + SystemClock.elapsedRealtime() + ".json" + ) + + configFile.parentFile?.mkdirs() + configFile.writeText(config) + cacheFiles.add(configFile) + + val commands = mutableListOf( + initPlugin("hysteria-plugin").path, + "--no-check", + "--config", + configFile.absolutePath, + "--log-level", + if (DataStore.enableLog) "trace" else "warn", + "client" + ) + + processes.start(commands) + } bean is ConfigBean -> { externalInstances[port]!!.launch() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index efa615f3..02c67475 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -210,6 +210,10 @@ object DataStore : OnPreferenceDataStoreChangeListener { var serverHeaders by profileCacheStore.string(Key.SERVER_HEADERS) var serverAllowInsecure by profileCacheStore.boolean(Key.SERVER_ALLOW_INSECURE) + var serverAuthType by profileCacheStore.stringToInt(Key.SERVER_AUTH_TYPE) + var serverUploadSpeed by profileCacheStore.stringToInt(Key.SERVER_UPLOAD_SPEED) + var serverDownloadSpeed by profileCacheStore.stringToInt(Key.SERVER_DOWNLOAD_SPEED) + var balancerType by profileCacheStore.stringToInt(Key.BALANCER_TYPE) var balancerGroup by profileCacheStore.stringToLong(Key.BALANCER_GROUP) var balancerStrategy by profileCacheStore.string(Key.BALANCER_STRATEGY) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt index 27644088..e98bf670 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt @@ -36,6 +36,8 @@ import io.nekohasekai.sagernet.fmt.brook.BrookBean import io.nekohasekai.sagernet.fmt.buildV2RayConfig import io.nekohasekai.sagernet.fmt.http.HttpBean import io.nekohasekai.sagernet.fmt.http.toUri +import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean +import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig import io.nekohasekai.sagernet.fmt.internal.BalancerBean import io.nekohasekai.sagernet.fmt.internal.ChainBean import io.nekohasekai.sagernet.fmt.internal.ConfigBean @@ -96,6 +98,7 @@ data class ProxyEntity( var ptBean: PingTunnelBean? = null, var rbBean: RelayBatonBean? = null, var brookBean: BrookBean? = null, + var hysteriaBean: HysteriaBean? = null, var configBean: ConfigBean? = null, var chainBean: ChainBean? = null, var balancerBean: BalancerBean? = null @@ -114,6 +117,7 @@ data class ProxyEntity( const val TYPE_PING_TUNNEL = 10 const val TYPE_RELAY_BATON = 11 const val TYPE_BROOK = 12 + const val TYPE_HYSTERIA = 15 const val TYPE_CHAIN = 8 const val TYPE_BALANCER = 14 @@ -173,6 +177,7 @@ data class ProxyEntity( TYPE_PING_TUNNEL -> ptBean = KryoConverters.pingTunnelDeserialize(byteArray) TYPE_RELAY_BATON -> rbBean = KryoConverters.relayBatonDeserialize(byteArray) TYPE_BROOK -> brookBean = KryoConverters.brookDeserialize(byteArray) + TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray) TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray) TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray) TYPE_BALANCER -> balancerBean = KryoConverters.balancerBeanDeserialize(byteArray) @@ -205,6 +210,7 @@ data class ProxyEntity( TYPE_PING_TUNNEL -> "PingTunnel" TYPE_RELAY_BATON -> "relaybaton" TYPE_BROOK -> "Brook" + TYPE_HYSTERIA -> "Hysteria" TYPE_CHAIN -> chainName TYPE_CONFIG -> configName TYPE_BALANCER -> balancerName @@ -214,23 +220,6 @@ data class ProxyEntity( fun displayName() = requireBean().displayName() fun displayAddress() = requireBean().displayAddress() - /*fun urlFixed(): String { - val bean = requireBean() - if (bean is ChainBean) { - if (bean.proxies.isNotEmpty()) { - val firstEntity = ProfileManager.getProfile(bean.proxies[0]) - if (firstEntity != null) { - return firstEntity.urlFixed(); - } - } - } - return if (Validator.isIpv6(bean.serverAddress)) { - "[${bean.serverAddress}]:${bean.serverPort}" - } else { - "${bean.serverAddress}:${bean.serverPort}" - } - }*/ - fun requireBean(): AbstractBean { return when (type) { TYPE_SOCKS -> socksBean @@ -245,8 +234,9 @@ data class ProxyEntity( TYPE_PING_TUNNEL -> ptBean TYPE_RELAY_BATON -> rbBean TYPE_BROOK -> brookBean - TYPE_CONFIG -> configBean + TYPE_HYSTERIA -> hysteriaBean + TYPE_CONFIG -> configBean TYPE_CHAIN -> chainBean TYPE_BALANCER -> balancerBean else -> error("Undefined type $type") @@ -277,6 +267,7 @@ data class ProxyEntity( is RelayBatonBean -> toUniversalLink() is BrookBean -> toUniversalLink() is ConfigBean -> toUniversalLink() + is HysteriaBean -> toUniversalLink() else -> null } } @@ -285,57 +276,54 @@ data class ProxyEntity( var name = "profile.json" return with(requireBean()) { - when (this) { - is ShadowsocksRBean -> buildShadowsocksRConfig() - is TrojanGoBean -> buildTrojanGoConfig(DataStore.socksPort, false) - is NaiveBean -> buildNaiveConfig(DataStore.socksPort) - is RelayBatonBean -> { - name = "profile.toml" - buildRelayBatonConfig(DataStore.socksPort) + StringBuilder().apply { + val config = buildV2RayConfig(this@ProxyEntity) + append(config.config) + + if (!config.index.all { it.chain.isEmpty() }) { + name = "profiles.txt" } - else -> StringBuilder().apply { - val config = buildV2RayConfig(this@ProxyEntity) - append(config.config) - if (!config.index.all { it.chain.isEmpty() }) { - name = "profiles.txt" - } + for ((isBalancer, chain) in config.index) { + chain.entries.forEachIndexed { index, (port, profile) -> + val needChain = !isBalancer && index != chain.size - 1 + val bean = profile.requireBean() + when { + profile.useExternalShadowsocks() -> { + bean as ShadowsocksBean + append("\n\n") + append(bean.buildShadowsocksConfig(port)) - for ((isBalancer, chain) in config.index) { - chain.entries.forEachIndexed { index, (port, profile) -> - val needChain = !isBalancer && index != chain.size - 1 - val bean = profile.requireBean() - when { - profile.useExternalShadowsocks() -> { - bean as ShadowsocksBean - append("\n\n") - append(bean.buildShadowsocksConfig(port)) - - } - bean is ShadowsocksRBean -> { - append("\n\n") - append(bean.buildShadowsocksRConfig()) - } - bean is TrojanGoBean -> { - append("\n\n") - append( - bean.buildTrojanGoConfig( - port, index == 0 && DataStore.enableMux - ) + } + bean is ShadowsocksRBean -> { + append("\n\n") + append(bean.buildShadowsocksRConfig()) + } + bean is TrojanGoBean -> { + append("\n\n") + append( + bean.buildTrojanGoConfig( + port, index == 0 && DataStore.enableMux ) - } - bean is NaiveBean -> { - append(bean.buildNaiveConfig(port)) - } - bean is RelayBatonBean -> { - append(bean.buildRelayBatonConfig(port)) - } + ) + } + bean is NaiveBean -> { + append("\n\n") + append(bean.buildNaiveConfig(port)) + } + bean is RelayBatonBean -> { + append("\n\n") + append(bean.buildRelayBatonConfig(port)) + } + bean is HysteriaBean -> { + append("\n\n") + append(bean.buildHysteriaConfig(port)) } } } - }.toString() - } to name - } + } + }.toString() + } to name } fun needExternal(): Boolean { @@ -343,20 +331,12 @@ data class ProxyEntity( TYPE_SOCKS -> false TYPE_HTTP -> false TYPE_SS -> useExternalShadowsocks() - TYPE_SSR -> true TYPE_VMESS -> false TYPE_VLESS -> false TYPE_TROJAN -> DataStore.providerTrojan != TrojanProvider.V2RAY - TYPE_TROJAN_GO -> true - TYPE_NAIVE -> true - TYPE_PING_TUNNEL -> true - TYPE_RELAY_BATON -> true - TYPE_BROOK -> true - TYPE_CONFIG -> true - TYPE_CHAIN -> false TYPE_BALANCER -> false - else -> error("Undefined type $type") + else -> true } } @@ -401,8 +381,9 @@ data class ProxyEntity( ptBean = null rbBean = null brookBean = null - configBean = null + hysteriaBean = null + configBean = null chainBean = null balancerBean = null @@ -455,6 +436,10 @@ data class ProxyEntity( type = TYPE_BROOK brookBean = bean } + is HysteriaBean -> { + type = TYPE_HYSTERIA + hysteriaBean = bean + } is ConfigBean -> { type = TYPE_CONFIG configBean = bean @@ -487,8 +472,9 @@ data class ProxyEntity( TYPE_PING_TUNNEL -> PingTunnelSettingsActivity::class.java TYPE_RELAY_BATON -> RelayBatonSettingsActivity::class.java TYPE_BROOK -> BrookSettingsActivity::class.java - TYPE_CONFIG -> ConfigSettingsActivity::class.java + TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java + TYPE_CONFIG -> ConfigSettingsActivity::class.java TYPE_CHAIN -> ChainSettingsActivity::class.java TYPE_BALANCER -> BalancerSettingsActivity::class.java else -> throw IllegalArgumentException() diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt index 537cb9de..294d4ad5 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.launch @Database( entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class, KeyValuePair::class], - version = 2 + version = 3 ) @TypeConverters(value = [KryoConverters::class, GsonConverters::class]) @GenerateRoomMigrations diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java index 6ef7a048..10ae287e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java @@ -34,6 +34,7 @@ import cn.hutool.core.util.ArrayUtil; import io.nekohasekai.sagernet.database.SubscriptionBean; import io.nekohasekai.sagernet.fmt.brook.BrookBean; import io.nekohasekai.sagernet.fmt.http.HttpBean; +import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean; import io.nekohasekai.sagernet.fmt.internal.BalancerBean; import io.nekohasekai.sagernet.fmt.internal.ChainBean; import io.nekohasekai.sagernet.fmt.internal.ConfigBean; @@ -163,6 +164,12 @@ public class KryoConverters { return deserialize(new BrookBean(), bytes); } + @TypeConverter + public static HysteriaBean hysteriaDeserialize(byte[] bytes) { + if (ArrayUtil.isEmpty(bytes)) return null; + return deserialize(new HysteriaBean(), bytes); + } + @TypeConverter public static ConfigBean configDeserialize(byte[] bytes) { if (ArrayUtil.isEmpty(bytes)) return null; diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt index c012be7c..019c7294 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt @@ -38,6 +38,7 @@ object TypeMap : HashMap() { this["rb"] = ProxyEntity.TYPE_RELAY_BATON this["brook"] = ProxyEntity.TYPE_BROOK this["config"] = ProxyEntity.TYPE_CONFIG + this["hysteria"] = ProxyEntity.TYPE_HYSTERIA } val reversed = HashMap() diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java new file mode 100644 index 00000000..ad60dd46 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java @@ -0,0 +1,115 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 . * + * * + ******************************************************************************/ + +package io.nekohasekai.sagernet.fmt.hysteria; + +import androidx.annotation.NonNull; + +import com.esotericsoftware.kryo.io.ByteBufferInput; +import com.esotericsoftware.kryo.io.ByteBufferOutput; + +import org.jetbrains.annotations.NotNull; + +import io.nekohasekai.sagernet.fmt.AbstractBean; +import io.nekohasekai.sagernet.fmt.KryoConverters; + +public class HysteriaBean extends AbstractBean { + + public static final int TYPE_NONE = 0; + public static final int TYPE_STRING = 1; + public static final int TYPE_BASE64 = 2; + + public Integer authPayloadType; + public String authPayload; + + public String obfuscation; + public String sni; + + public Integer uploadMbps; + public Integer downloadMbps; + public Boolean allowInsecure; + + @Override + public void initializeDefaultValues() { + super.initializeDefaultValues(); + if (authPayloadType == null) authPayloadType = TYPE_NONE; + if (authPayload == null) authPayload = ""; + if (obfuscation == null) obfuscation = ""; + if (sni == null) sni = ""; + if (uploadMbps == null) uploadMbps = 10; + if (downloadMbps == null) downloadMbps = 50; + if (allowInsecure == null) allowInsecure = false; + } + + @Override + public void serialize(ByteBufferOutput output) { + output.writeInt(0); + super.serialize(output); + output.writeInt(authPayloadType); + output.writeString(authPayload); + output.writeString(obfuscation); + output.writeString(sni); + output.writeInt(uploadMbps); + output.writeInt(downloadMbps); + output.writeBoolean(allowInsecure); + } + + @Override + public void deserialize(ByteBufferInput input) { + int version = input.readInt(); + super.deserialize(input); + authPayloadType = input.readInt(); + authPayload = input.readString(); + obfuscation = input.readString(); + sni = input.readString(); + uploadMbps = input.readInt(); + downloadMbps = input.readInt(); + allowInsecure = input.readBoolean(); + } + + @Override + public void applyFeatureSettings(AbstractBean other) { + if (!(other instanceof HysteriaBean)) return; + HysteriaBean bean = ((HysteriaBean) other); + bean.uploadMbps = uploadMbps; + bean.downloadMbps = downloadMbps; + bean.allowInsecure = allowInsecure; + } + + @NotNull + @Override + public HysteriaBean clone() { + return KryoConverters.deserialize(new HysteriaBean(), KryoConverters.serialize(this)); + } + + public static final Creator CREATOR = new CREATOR() { + @NonNull + @Override + public HysteriaBean newInstance() { + return new HysteriaBean(); + } + + @Override + public HysteriaBean[] newArray(int size) { + return new HysteriaBean[size]; + } + }; +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt new file mode 100644 index 00000000..230bbfbd --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt @@ -0,0 +1,64 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 . * + * * + ******************************************************************************/ + +package io.nekohasekai.sagernet.fmt.hysteria + +import cn.hutool.core.util.NumberUtil +import cn.hutool.json.JSONObject +import io.nekohasekai.sagernet.fmt.LOCALHOST + +fun JSONObject.parseHysteria(): HysteriaBean { + return HysteriaBean().apply { + serverAddress = getStr("server").substringBefore(":") + serverPort = getStr("server").substringAfter(":") + .takeIf { NumberUtil.isInteger(it) } + ?.toInt() ?: 443 + uploadMbps = getInt("up_mbps") + downloadMbps = getInt("down_mbps") + obfuscation = getStr("obfs") + getStr("auth")?.also { + authPayloadType = HysteriaBean.TYPE_BASE64 + authPayload = it + } + getStr("auth_str")?.also { + authPayloadType = HysteriaBean.TYPE_STRING + authPayload = it + } + sni = getStr("server_name") + allowInsecure = getBool("insecure") + } +} + +fun HysteriaBean.buildHysteriaConfig(port: Int): String { + return JSONObject().also { + it["server"] = "$serverAddress:$serverPort" + it["up_mbps"] = uploadMbps + it["down_mbps"] = downloadMbps + it["socks5"] = JSONObject(mapOf("listen" to "$LOCALHOST:$port")) + it["obfs"] = obfuscation + when (authPayloadType) { + HysteriaBean.TYPE_BASE64 -> it["auth"] = authPayload + HysteriaBean.TYPE_STRING -> it["auth_str"] = authPayload + } + if (sni.isNotBlank()) it["server_name"] = sni + if (allowInsecure) it["insecure"] = true + }.toStringPretty() +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt index d78f2156..747a17ae 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt @@ -22,6 +22,7 @@ package io.nekohasekai.sagernet.fmt.shadowsocks import cn.hutool.core.codec.Base64 +import cn.hutool.json.JSONObject import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginOptions @@ -30,7 +31,6 @@ import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.fmt.LOCALHOST import io.nekohasekai.sagernet.ktx.* import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import cn.hutool.json.JSONObject as HSONObject val methodsV2fly = arrayOf( "none", "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305" @@ -179,7 +179,7 @@ fun ShadowsocksBean.toUri(): String { } -fun HSONObject.parseShadowsocks(): ShadowsocksBean { +fun JSONObject.parseShadowsocks(): ShadowsocksBean { return ShadowsocksBean().apply { var pluginStr = "" val pId = getStr("plugin") @@ -207,7 +207,7 @@ fun ShadowsocksBean.buildShadowsocksConfig(port: Int): String { throw IllegalArgumentException("Cipher $method is deprecated.") } - val proxyConfig = HSONObject().also { + val proxyConfig = JSONObject().also { it["server"] = finalAddress it["server_port"] = finalPort it["method"] = method diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt index b204268e..c1254774 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt @@ -31,6 +31,7 @@ import io.nekohasekai.sagernet.database.SubscriptionBean import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.brook.BrookBean import io.nekohasekai.sagernet.fmt.http.HttpBean +import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean import io.nekohasekai.sagernet.fmt.naive.NaiveBean import io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean import io.nekohasekai.sagernet.fmt.socks.SOCKSBean @@ -161,6 +162,9 @@ abstract class GroupUpdater { is TrojanGoBean -> { if (sni.isBlank()) sni = bean.serverAddress } + is HysteriaBean -> { + if (sni.isBlank()) sni = bean.serverAddress + } } bean.serverAddress = address diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt index def921ce..d0ca82b9 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt @@ -30,6 +30,7 @@ import io.nekohasekai.sagernet.database.* import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.gson.gson import io.nekohasekai.sagernet.fmt.http.HttpBean +import io.nekohasekai.sagernet.fmt.hysteria.parseHysteria import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean import io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks @@ -450,6 +451,9 @@ object RawUpdater : GroupUpdater() { json.containsKey("remote_addr") -> { return listOf(json.parseTrojanGo()) } + json.containsKey("up_mbps") -> { + return listOf(json.parseHysteria()) + } else -> json.forEach { _, it -> if (it is JSON) { proxies.addAll(parseJSON(it)) diff --git a/app/src/main/java/io/nekohasekai/sagernet/tun/DirectTunThread.kt b/app/src/main/java/io/nekohasekai/sagernet/tun/DirectTunThread.kt index 2208b3a2..f6d72331 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/tun/DirectTunThread.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/tun/DirectTunThread.kt @@ -180,6 +180,10 @@ class DirectTunThread(val service: VpnService) : Thread("TUN Thread") { processOther(buffer, length) } } + } catch (e: SecurityException) { + interrupt() + running = false + return } catch (e: InterruptedException) { interrupt() running = false diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index f8713cf8..bbd2380b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -290,6 +290,10 @@ class ConfigurationFragment @JvmOverloads constructor( R.id.action_new_brook -> { startActivity(Intent(requireActivity(), BrookSettingsActivity::class.java)) } + R.id.action_new_hysteria -> { + startActivity(Intent(requireActivity(), HysteriaSettingsActivity::class.java)) + } + R.id.action_new_config -> { startActivity(Intent(requireActivity(), ConfigSettingsActivity::class.java)) } @@ -1279,10 +1283,8 @@ class ConfigurationFragment @JvmOverloads constructor( val pf = requireParentFragment() as ConfigurationFragment - if (proxyEntity.requireBean().name.isNotBlank()) { - if (!pf.alwaysShowAddress) { - address = "" - } + if (proxyEntity.requireBean().name.isBlank() || !pf.alwaysShowAddress) { + address = "" } profileAddress.text = address @@ -1346,13 +1348,17 @@ class ConfigurationFragment @JvmOverloads constructor( popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(R.id.action_v2rayn_clipboard) } - if (proxyEntity.configBean != null) { - - popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_standard_qr) - popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(R.id.action_standard_clipboard) - } else if (!proxyEntity.haveLink()) { - popup.menu.removeItem(R.id.action_group_qr) - popup.menu.removeItem(R.id.action_group_clipboard) + when { + proxyEntity.configBean != null || proxyEntity.hysteriaBean != null -> { + popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_standard_qr) + popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem( + R.id.action_standard_clipboard + ) + } + !proxyEntity.haveLink() -> { + popup.menu.removeItem(R.id.action_group_qr) + popup.menu.removeItem(R.id.action_group_clipboard) + } } if (proxyEntity.ptBean != null || proxyEntity.brookBean != null) { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt new file mode 100644 index 00000000..0a372954 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt @@ -0,0 +1,95 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * 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 . * + * * + ******************************************************************************/ + +package io.nekohasekai.sagernet.ui.profile + +import android.os.Bundle +import androidx.preference.EditTextPreference +import com.takisoft.preferencex.PreferenceFragmentCompat +import com.takisoft.preferencex.SimpleMenuPreference +import io.nekohasekai.sagernet.Key +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers +import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean +import io.nekohasekai.sagernet.ktx.applyDefaultValues + +class HysteriaSettingsActivity : ProfileSettingsActivity() { + + override fun createEntity() = HysteriaBean().applyDefaultValues() + + override fun HysteriaBean.init() { + DataStore.profileName = name + DataStore.serverAddress = serverAddress + DataStore.serverPort = serverPort + DataStore.serverObfs = obfuscation + DataStore.serverAuthType = authPayloadType + DataStore.serverPassword = authPayload + DataStore.serverSNI = sni + DataStore.serverAllowInsecure = allowInsecure + DataStore.serverUploadSpeed = uploadMbps + DataStore.serverDownloadSpeed = downloadMbps + } + + override fun HysteriaBean.serialize() { + name = DataStore.profileName + serverAddress = DataStore.serverAddress + serverPort = DataStore.serverPort + obfuscation = DataStore.serverObfs + authPayloadType = DataStore.serverAuthType + authPayload = DataStore.serverPassword + sni = DataStore.serverSNI + allowInsecure = DataStore.serverAllowInsecure + uploadMbps = DataStore.serverUploadSpeed + downloadMbps = DataStore.serverDownloadSpeed + } + + override fun PreferenceFragmentCompat.createPreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + addPreferencesFromResource(R.xml.hysteria_preferences) + + val authType = findPreference(Key.SERVER_AUTH_TYPE)!! + val authPayload = findPreference(Key.SERVER_PASSWORD)!! + authPayload.isVisible = authType.value != "${HysteriaBean.TYPE_NONE}" + authType.setOnPreferenceChangeListener { _, newValue -> + authPayload.isVisible = newValue != "${HysteriaBean.TYPE_NONE}" + true + } + + findPreference(Key.SERVER_UPLOAD_SPEED)!!.apply { + setOnBindEditTextListener(EditTextPreferenceModifiers.Port) + } + findPreference(Key.SERVER_DOWNLOAD_SPEED)!!.apply { + setOnBindEditTextListener(EditTextPreferenceModifiers.Port) + } + + findPreference(Key.SERVER_PORT)!!.apply { + setOnBindEditTextListener(EditTextPreferenceModifiers.Port) + } + + findPreference(Key.SERVER_PASSWORD)!!.apply { + summaryProvider = PasswordSummaryProvider + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_download_24.xml b/app/src/main/res/drawable/ic_baseline_download_24.xml new file mode 100644 index 00000000..1f61509e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_download_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/menu/add_profile_menu.xml b/app/src/main/res/menu/add_profile_menu.xml index bac045a7..cfb2c60e 100644 --- a/app/src/main/res/menu/add_profile_menu.xml +++ b/app/src/main/res/menu/add_profile_menu.xml @@ -54,6 +54,10 @@ + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7b0d2b62..5f94262d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -377,4 +377,9 @@ 选择应用 %d 个应用 要支持基于应用的路由, VPN 模式必须为原生转发. + 混淆密码 + 认证类型 + 认证载荷 + 最大上行 (Mbps) + 最大下行 (Mbps) \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index cbd016d2..48590dda 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -611,5 +611,11 @@ SIP008 + + @string/plugin_disabled + STRING + BASE64 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2bcc71f2..644308c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,6 +213,7 @@ Ping Tunnel RelayBaton Brook + Hysteria Proxy Chain Custom Config Balancer @@ -395,4 +396,9 @@ Select Applications %d Applications To support application-based routing, the VPN Mode must be Native Forwarding. + Obfuscation Password + Authentication Type + Authentication Payload + Max Upload Speed (in Mbps) + Max Download Speed (in Mbps) \ No newline at end of file diff --git a/app/src/main/res/xml/hysteria_preferences.xml b/app/src/main/res/xml/hysteria_preferences.xml new file mode 100644 index 00000000..3f4c84df --- /dev/null +++ b/app/src/main/res/xml/hysteria_preferences.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sager.properties b/sager.properties index 96caba83..a4122e5a 100644 --- a/sager.properties +++ b/sager.properties @@ -1,6 +1,6 @@ PACKAGE_NAME=io.nekohasekai.sagernet -VERSION_NAME=0.3-rc05 -VERSION_CODE=71 +VERSION_NAME=0.3-rc06 +VERSION_CODE=72 NAIVE_VERSION_NAME=92.0.4515.107-1 NAIVE_VERSION=3