diff --git a/.github/ISSUE_TEMPLATE/bug-report-en.md b/.github/ISSUE_TEMPLATE/bug-report-en.md new file mode 100644 index 000000000..5390a49fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-en.md @@ -0,0 +1,22 @@ +--- +name: 'Bug Report' +about: 'Please troubleshoot server-side issues and upgrade to the latest client before raising a question.' +title: 'BUG: ' +labels: '' +assignees: '' + +--- + +## Describe the problem + +Expected behavior: + +Actual behavior: + +## How to reproduce + +Provide helpful screenshots, videos, text descriptions, subscription links, etc. + +## log + +If you have logs, please upload them. Please see the detailed steps for exporting logs in the documentation. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report-zh_cn.md b/.github/ISSUE_TEMPLATE/bug-report-zh_cn.md index d9bbafdf8..232e6a56a 100644 --- a/.github/ISSUE_TEMPLATE/bug-report-zh_cn.md +++ b/.github/ISSUE_TEMPLATE/bug-report-zh_cn.md @@ -1,22 +1,22 @@ --- -name: Bug Report zh_CN -about: 问题反馈,在提出问题前请先自行排除服务器端问题和升级到最新客户端。 -title: '' +name: '问题反馈' +about: '在提出问题前请先自行排除服务器端问题和升级到最新客户端。' +title: 'BUG: ' labels: '' assignees: '' --- -**描述问题** +## 描述问题 预期行为: 实际行为: -**如何复现** +## 如何复现 提供有帮助的截图,录像,文字说明,订阅链接等。 -**日志** +## 日志 如果有日志,请上传。请在文档内查看导出日志的详细步骤。 diff --git a/.github/ISSUE_TEMPLATE/feature_request-en.md b/.github/ISSUE_TEMPLATE/feature_request-en.md new file mode 100644 index 000000000..e688400c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request-en.md @@ -0,0 +1,12 @@ +--- +name: 'Feature Request' +about: 'Make suggestions for new features of the software' +title: '' +labels: '' +assignees: '' + +--- + +## Description suggestions + +## Necessity of recommendations diff --git a/.github/ISSUE_TEMPLATE/feature_request-zh_cn.md b/.github/ISSUE_TEMPLATE/feature_request-zh_cn.md index b8a79e8d5..93baf216d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request-zh_cn.md +++ b/.github/ISSUE_TEMPLATE/feature_request-zh_cn.md @@ -1,12 +1,12 @@ --- -name: Feature Request zh_CN -about: 功能请求,提出建议。 +name: '功能请求' +about: '对软件的新功能提出建议。' title: '' labels: '' assignees: '' --- -**描述建议** +## 描述建议 -**建议的必要性** +## 建议的必要性 diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 000000000..a6df3a164 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,69 @@ +name: Preview Build +on: + workflow_dispatch: + inputs: +jobs: + libcore: + name: Native Build (LibCore) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Golang Status + run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status + - name: Libcore Status + run: git ls-files libcore | xargs cat | sha1sum > libcore_status + - name: LibCore Cache + id: cache + uses: actions/cache@v4 + with: + path: | + app/libs/libcore.aar + key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} + - name: Install Golang + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: ^1.25 + - name: Native Build + if: steps.cache.outputs.cache-hit != 'true' + run: ./run lib core + build: + name: Build OSS APK + runs-on: ubuntu-latest + needs: + - libcore + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Golang Status + run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status + - name: Libcore Status + run: git ls-files libcore | xargs cat | sha1sum > libcore_status + - name: LibCore Cache + uses: actions/cache@v4 + with: + path: | + app/libs/libcore.aar + key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} + - name: Gradle cache + uses: actions/cache@v4 + with: + path: ~/.gradle + key: gradle-oss-${{ hashFiles('**/*.gradle.kts') }} + - name: Gradle Build + env: + BUILD_PLUGIN: none + run: | + echo "sdk.dir=${ANDROID_HOME}" > local.properties + echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties + export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" + ./run init action gradle + ./gradlew app:assemblePreviewRelease + APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') + APK=$(dirname $APK) + echo "APK=$APK" >> $GITHUB_ENV + - uses: actions/upload-artifact@v4 + with: + name: APKs + path: ${{ env.APK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8f61485e..7aecc1c05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,16 +3,13 @@ on: workflow_dispatch: inputs: tag: - description: 'Release Tag' + description: "Release Tag" required: true - upload: - description: 'Upload: If want ignore' - required: false publish: - description: 'Publish: If want ignore' + description: "Publish: If want ignore" required: false play: - description: 'Play: If want ignore' + description: "Play: If want ignore" required: false jobs: libcore: @@ -20,27 +17,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Golang Status run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status - name: Libcore Status run: git ls-files libcore | xargs cat | sha1sum > libcore_status - name: LibCore Cache id: cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/libs/libcore.aar key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} - - name: Golang Cache + - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/cache@v3 + uses: actions/setup-go@v5 with: - path: build/golang - key: go-${{ hashFiles('.github/workflows/*', 'golang_status') }} + go-version: ^1.25 - name: Native Build if: steps.cache.outputs.cache-hit != 'true' - run: ./run init action go && ./run lib core + run: ./run lib core build: name: Build OSS APK runs-on: ubuntu-latest @@ -48,19 +44,19 @@ jobs: - libcore steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Golang Status run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status - name: Libcore Status run: git ls-files libcore | xargs cat | sha1sum > libcore_status - name: LibCore Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/libs/libcore.aar key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} - name: Gradle cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.gradle key: gradle-oss-${{ hashFiles('**/*.gradle.kts') }} @@ -76,7 +72,7 @@ jobs: APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') APK=$(dirname $APK) echo "APK=$APK" >> $GITHUB_ENV - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: APKs path: ${{ env.APK }} @@ -87,9 +83,9 @@ jobs: needs: build steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Donwload Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: APKs path: artifacts @@ -101,36 +97,6 @@ jobs: mkdir apks find artifacts -name "*.apk" -exec cp {} apks \; ./ghr -delete -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@v3 - 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: Build Play Bundle if: github.event.inputs.play != 'y' @@ -139,19 +105,19 @@ jobs: - libcore steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Golang Status run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status - name: Libcore Status run: git ls-files libcore | xargs cat | sha1sum > libcore_status - name: LibCore Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/libs/libcore.aar key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} - name: Gradle cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.gradle key: gradle-play-${{ hashFiles('**/*.gradle.kts') }} diff --git a/README.md b/README.md index dfdc0c515..b1322451e 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,20 @@ sing-box / universal proxy toolchain for Android. -## 下载 / Downloads +一款使用 sing-box 的 Android 通用代理软件. -### GitHub Releases +## 下载 / Downloads [![GitHub All Releases](https://img.shields.io/github/downloads/Matsuridayo/NekoBoxForAndroid/total?label=downloads-total&logo=github&style=flat-square)](https://github.com/Matsuridayo/NekoBoxForAndroid/releases) -[下载](https://github.com/Matsuridayo/NekoBoxForAndroid/releases) +[GitHub Releases 下载](https://github.com/Matsuridayo/NekoBoxForAndroid/releases) -[Google Play](https://play.google.com/store/apps/details?id=moe.nb4a) +**Google Play 版本自 2024 年 5 月起已被第三方控制,为非开源版本,请不要下载。** -## 更改记录 & 发布频道 / Changelog & Telegram channel +**The Google Play version has been controlled by a third party since May 2024 and is a non-open +source version. Please do not download it.** + +## 更新日志 & Telegram 发布频道 / Changelog & Telegram Channel https://t.me/Matsuridayo @@ -24,34 +27,53 @@ https://t.me/Matsuridayo https://matsuridayo.github.io -## 代理 / Proxy +## 支持的代理协议 / Supported Proxy Protocols * SOCKS (4/4a/5) * HTTP(S) * SSH * Shadowsocks * VMess +* Trojan * VLESS +* AnyTLS +* ShadowTLS +* TUIC +* Hysteria 1/2 * WireGuard -* Trojan -* Trojan-Go ( trojan-go-plugin ) -* NaïveProxy ( naive-plugin ) -* Hysteria ( hysteria-plugin ) +* Trojan-Go (trojan-go-plugin) +* NaïveProxy (naive-plugin) +* Mieru (mieru-plugin) + +请到[这里](https://matsuridayo.github.io/nb4a-plugin/)下载插件以获得完整的代理支持. + +Please visit [here](https://matsuridayo.github.io/nb4a-plugin/) to download plugins for full proxy +supports. + +## 支持的订阅格式 / Supported Subscription Format -请到[项目主页](https://matsuridayo.github.io)下载插件。 +* 一些广泛使用的格式 (如 Shadowsocks, ClashMeta 和 v2rayN) +* sing-box 出站 -Please go to the [project homepage](https://matsuridayo.github.io) to download plugins. +仅支持解析出站,即节点。分流规则等信息会被忽略。 -## 订阅 / Subscription +* Some widely used formats (like Shadowsocks, ClashMeta and v2rayN) +* sing-box outbound -* Raw: some widely used formats (like shadowsocks, clash and v2rayN) -* 原始格式:一些广泛使用的格式(如 shadowsocks、clash 和 v2rayN) +Only resolving outbound, i.e. nodes, is supported. Information such as diversion rules are ignored. ## 捐助 / Donate -欢迎捐赠以支持项目开发。 +
-您也可以通过 [Google Play](https://play.google.com/store/apps/details?id=moe.nb4a) 购买捐赠 +如果这个项目对您有帮助, 可以通过捐赠的方式帮助我们维持这个项目. + +捐赠满等额 50 USD 可以在「[捐赠榜](https://mtrdnt.pages.dev/donation_list)」显示头像, 如果您未被添加到这里, +欢迎联系我们补充. + +Donations of 50 USD or more can display your avatar on +the [Donation List](https://mtrdnt.pages.dev/donation_list). If you are not added here, please +contact us to add it. USDT TRC20 @@ -61,13 +83,19 @@ XMR `49bwESYQjoRL3xmvTcjZKHEKaiGywjLYVQJMUv79bXonGiyDCs8AzE3KiGW2ytTybBCpWJUvov8SjZZEGg66a4e59GXa6k5` +
+ ## Credits Core: + - [SagerNet/sing-box](https://github.com/SagerNet/sing-box) -- [Matsuridayo/sing-box-extra](https://github.com/MatsuriDayo/sing-box-extra) Android GUI: + - [shadowsocks/shadowsocks-android](https://github.com/shadowsocks/shadowsocks-android) - [SagerNet/SagerNet](https://github.com/SagerNet/SagerNet) -- [Matsuridayo/Matsuri](https://github.com/MatsuriDayo/Matsuri) + +Web Dashboard: + +- [Yacd-meta](https://github.com/MetaCubeX/Yacd-meta) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 054a79591..a8ba7b0e2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,9 @@ +@file:Suppress("UnstableApiUsage") + plugins { id("com.android.application") id("kotlin-android") - id("kotlin-kapt") + id("com.google.devtools.ksp") id("kotlin-parcelize") } @@ -11,8 +13,8 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true } - kapt.arguments { - arg("room.incremental", true) + ksp { + arg("room.incremental", "true") arg("room.schemaLocation", "$projectDir/schemas") } bundle { @@ -21,9 +23,19 @@ android { } } buildFeatures { + buildConfig = true viewBinding = true + aidl = true } namespace = "io.nekohasekai.sagernet" + packaging { + jniLibs { + useLegacyPackaging = true + } + } + androidResources { + generateLocaleConfig = true + } } dependencies { @@ -33,7 +45,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.recyclerview:recyclerview:1.3.0") - implementation("androidx.activity:activity-ktx:1.7.0") + implementation("androidx.activity:activity-ktx:1.10.1") implementation("androidx.fragment:fragment-ktx:1.5.6") implementation("androidx.browser:browser:1.5.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -65,15 +77,12 @@ dependencies { exclude(group = "androidx.recyclerview") exclude(group = "androidx.appcompat") } - implementation("org.smali:dexlib2:2.5.2") { - exclude(group = "com.google.guava", module = "guava") - } - implementation("androidx.room:room-runtime:2.5.1") - kapt("androidx.room:room-compiler:2.5.1") - implementation("androidx.room:room-ktx:2.5.1") + implementation("androidx.room:room-runtime:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") implementation("com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4") - kapt("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4") + ksp("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") } diff --git a/app/executableSo/.gitignore b/app/executableSo/.gitignore new file mode 100644 index 000000000..140f8cf80 --- /dev/null +++ b/app/executableSo/.gitignore @@ -0,0 +1 @@ +*.so 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 000000000..491eb6f81 --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json @@ -0,0 +1,360 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "cff00d0142d9e53d2ca24a6a55cd213c", + "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, `order` INTEGER NOT NULL, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelector", + "columnName": "isSelector", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` 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": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "tuicBean", + "columnName": "tuicBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "shadowTLSBean", + "columnName": "shadowTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "nekoBean", + "columnName": "nekoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "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, `outbound` INTEGER 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": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, 'cff00d0142d9e53d2ca24a6a55cd213c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/4.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/4.json new file mode 100644 index 000000000..4f9fe2659 --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/4.json @@ -0,0 +1,360 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "cff00d0142d9e53d2ca24a6a55cd213c", + "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, `order` INTEGER NOT NULL, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelector", + "columnName": "isSelector", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` 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": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "tuicBean", + "columnName": "tuicBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "shadowTLSBean", + "columnName": "shadowTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "nekoBean", + "columnName": "nekoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "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, `outbound` INTEGER 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": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, 'cff00d0142d9e53d2ca24a6a55cd213c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/5.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/5.json new file mode 100644 index 000000000..b5af39f95 --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/5.json @@ -0,0 +1,366 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "1dbf667053726c13d139a4d83c41f895", + "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, `order` INTEGER NOT NULL, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelector", + "columnName": "isSelector", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `anyTLSBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` 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": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "tuicBean", + "columnName": "tuicBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "shadowTLSBean", + "columnName": "shadowTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "anyTLSBean", + "columnName": "anyTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "nekoBean", + "columnName": "nekoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "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, `outbound` INTEGER 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": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, '1dbf667053726c13d139a4d83c41f895')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json new file mode 100644 index 000000000..11b6e58a2 --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json @@ -0,0 +1,373 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "3d3db9106a89d6f20ef3fde6e81dbaa9", + "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, `order` INTEGER NOT NULL, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` INTEGER NOT NULL)", + "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 + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelector", + "columnName": "isSelector", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `anyTLSBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` 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": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "tuicBean", + "columnName": "tuicBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "shadowTLSBean", + "columnName": "shadowTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "anyTLSBean", + "columnName": "anyTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "nekoBean", + "columnName": "nekoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "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, `config` TEXT NOT NULL DEFAULT '', `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, `outbound` INTEGER NOT NULL, `packages` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "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": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, '3d3db9106a89d6f20ef3fde6e81dbaa9')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4607b16ed..def29c24c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + @@ -21,7 +22,9 @@ + tools:ignore="PackageVisibilityPolicy" /> + + @@ -50,7 +53,6 @@ android:autoRevokePermissions="allowed" android:banner="@mipmap/ic_launcher" android:dataExtractionRules="@xml/backup_rules" - android:extractNativeLibs="true" android:fullBackupContent="@xml/backup_descriptor" android:fullBackupOnly="true" android:hardwareAccelerated="true" @@ -164,6 +166,9 @@ + @@ -186,7 +191,7 @@ android:name="moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity" android:configChanges="uiMode" /> + android:foregroundServiceType="systemExempted" + android:process=":bg" + tools:ignore="ForegroundServicePermission" /> + android:process=":bg" + tools:ignore="ForegroundServicePermission"> @@ -269,10 +278,12 @@ @@ -297,6 +308,18 @@ android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + + + + + + + diff --git a/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl b/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl index f0382acc1..cb41c2bd7 100644 --- a/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl +++ b/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl @@ -6,7 +6,7 @@ import io.nekohasekai.sagernet.aidl.TrafficData; oneway interface ISagerNetServiceCallback { void stateChanged(int state, String profileName, String msg); void missingPlugin(String profileName, String pluginName); - void routeAlert(int type, String routeName); void cbSpeedUpdate(in SpeedDisplayData stats); void cbTrafficUpdate(in TrafficData stats); + void cbSelectorUpdate(long id); } diff --git a/app/src/main/assets/analysis.txt b/app/src/main/assets/analysis.txt deleted file mode 100644 index a4f7cc732..000000000 --- a/app/src/main/assets/analysis.txt +++ /dev/null @@ -1,5 +0,0 @@ -domain:appcenter.ms -domain:app-measurement.com -domain:firebase.io -domain:crashlytics.com -domain:google-analytics.com \ No newline at end of file diff --git a/app/src/main/assets/proxy_packagename.txt b/app/src/main/assets/proxy_packagename.txt new file mode 100644 index 000000000..28768cf02 --- /dev/null +++ b/app/src/main/assets/proxy_packagename.txt @@ -0,0 +1,390 @@ +amanita_design.samorost3.gp +android +au.com.shiftyjelly.pocketcasts +bbc.mobile.news.ww +be.mygod.vpnhotspot +ch.protonmail.android +cm.aptoide.pt +co.wanqu.android +com.alphainventor.filemanager +com.amazon.kindle +com.amazon.mshop.android.shopping +com.android.chrome +com.android.providers.downloads +com.android.providers.downloads.ui +com.android.providers.telephony +com.android.settings +com.android.vending +com.android6park.m6park +com.apkpure.aegon +com.apkupdater +com.app.pornhub +com.arthurivanets.owly +com.asahi.tida.tablet +com.authy.authy +com.avmovie +com.ballistiq.artstation +com.binance.dev +com.bitly.app +com.brave.browser +com.brave.browser_beta +com.breel.wallpapers18 +com.bvanced.android.youtube +com.chrome.beta +com.chrome.canary +com.chrome.dev +com.cl.newt66y +com.cradle.iitc_mobile +org.exarhteam.iitc_mobile +com.cygames.shadowverse +com.dcard.freedom +com.devhd.feedly +com.devolver.reigns2 +com.discord +com.downloader.video.tumblr +com.driverbrowser +com.dropbox.android +com.duolingo +com.duckduckgo.mobile.android +com.dv.adm +com.estrongs.android.pop +com.estrongs.android.pop.pro +com.evernote +com.facebook.katana +com.facebook.lite +com.facebook.mlite +com.facebook.orca +com.facebook.services +com.facebook.system +com.fastaccess.github +com.felixfilip.scpae +com.fireproofstudios.theroom4 +com.firstrowria.pushnotificationtester +com.flyersoft.moonreaderp +com.fooview.android.fooview +com.fvd.eversync +com.gameloft.android.anmp.glofta8hm +com.gameloft.android.anmp.glofta9hm +com.gianlu.aria2app +com.github.yeriomin.yalpstore +com.google.android.apps.adm +com.google.android.apps.books +com.google.android.apps.docs +com.google.android.apps.docs.editors.sheets +com.google.android.apps.docs.editors.docs +com.google.android.apps.docs.editors.slides +com.google.android.apps.fitness +com.google.android.apps.googleassistant +com.google.android.apps.googlevoice +com.google.android.apps.hangoutsdialer +com.google.android.apps.inbox +com.google.android.apps.magazines +com.google.android.apps.maps +com.google.android.apps.nbu.files +com.google.android.apps.paidtasks +com.google.android.apps.pdfviewer +com.google.android.apps.photos +com.google.android.apps.plus +com.google.android.apps.translate +com.google.android.gm +com.google.android.gms +com.google.android.gms.setup +com.google.android.googlequicksearchbox +com.google.android.gsf +com.google.android.gsf.login +com.google.android.ims +com.google.android.inputmethod.latin +com.google.android.instantapps.supervisor +com.google.android.keep +com.google.android.music +com.google.android.ogyoutube +com.google.android.partnersetup +com.google.android.play.games +com.google.android.street +com.google.android.syncadapters.calendar +com.google.android.syncadapters.contacts +com.google.android.talk +com.google.android.tts +com.google.android.videos +com.google.android.youtube +com.google.ar.lens +com.google.android.apps.authenticator2 +com.hochan.coldsoup +com.ifttt.ifttt +com.imgur.mobile +com.innologica.inoreader +com.instagram.android +com.instagram.lite +com.instapaper.android +com.jarvanh.vpntether +com.kapp.youtube.final +com.klinker.android.twitter_l +com.lastpass.lpandroid +com.linecorp.linelite +com.lingodeer +com.ltnnews.news +com.mediapods.tumbpods +com.mgoogle.android.gms +com.microsoft.emmx +com.microsoft.office.powerpoint +com.microsoft.skydrive +com.mixplorer +com.msd.consumerchinese +com.msd.professionalchinese +com.mss2011c.sharehelper +com.netflix.mediaclient +com.newin.nplayer.pro +com.nianticlabs.ingress.prime.qa +com.nianticproject.ingress +com.ninefolders.hd3 +com.ninegag.android.app +com.nintendo.zara +com.nytimes.cn +com.oasisfeng.island +com.ocnt.liveapp.hw +com.orekie.search +com.patreon.android +com.paypal.android.p2pmobile +com.perol.asdpl.pixivez +com.pinterest +com.popularapp.periodcalendar +com.popularapp.videodownloaderforinstagram +com.pushbullet.android +com.quoord.tapatalkpro.activity +com.quora.android +com.rayark.cytus2 +com.rayark.implosion +com.rayark.pluto +com.reddit.frontpage +com.resilio.sync +com.rhmsoft.edit +com.rubenmayayo.reddit +com.sec.android.app.sbrowser +com.sec.android.app.sbrowser.beta +com.shanga.walli +com.simplehabit.simplehabitapp +com.slack +com.snaptube.premium +com.sololearn +com.sonelli.juicessh +com.sparkslab.dcardreader +com.spotify.music +com.spotify.lite +com.tencent.huatuo +com.termux +com.teslacoilsw.launcher +com.theinitium.news +com.thomsonreuters.reuters +com.thunkable.android.hritvik00.freenom +com.topjohnwu.magisk +com.tripadvisor.tripadvisor +com.tumblr +com.twitter.android +com.u91porn +com.u9porn +com.ubisoft.dance.justdance2015companion +com.udn.news +com.utopia.pxview +com.valvesoftware.android.steam.community +com.vanced.manager +com.vanced.android.youtube +com.vanced.android.apps.youtube.music +com.mgoogle.android.gms +com.vimeo.android.videoapp +com.vivaldi.browser +com.vivaldi.browser.snapshot +com.vkontakte.android +com.whatsapp +com.wire +com.wuxiangai.refactor +com.xda.labs +com.xvideos.app +com.yahoo.mobile.client.android.superapp +com.yandex.browser +com.yandex.browser.beta +com.yandex.browser.alpha +com.z28j.feel +com.zhiliaoapp.musically +con.medium.reader +de.apkgrabber +de.robv.android.xposed.installer +dk.tacit.android.foldersync.full +es.rafalense.telegram.themes +es.rafalense.themes +flipboard.app +fm.moon.app +fr.gouv.etalab.mastodon +github.tornaco.xposedmoduletest +idm.internet.download.manager +idm.internet.download.manager.plus +io.github.javiewer +io.github.skyhacker2.magnetsearch +io.va.exposed +it.mvilla.android.fenix2 +jp.bokete.app.android +jp.naver.line.android +jp.pxv.android +luo.speedometergpspro +m.cna.com.tw.App +mark.via.gp +me.tshine.easymark +net.teeha.android.url_shortener +net.tsapps.appsales +onion.fire +org.fdroid.fdroid +org.freedownloadmanager.fdm +org.kustom.widget +org.mozilla.fennec_aurora +org.mozilla.fenix +org.mozilla.fenix.nightly +org.mozilla.firefox +org.mozilla.firefox_beta +org.mozilla.focus +org.schabi.newpipe +org.telegram.messenger +org.telegram.multi +org.telegram.plus +org.thunderdog.challegram +org.torproject.android +org.torproject.torbrowser_alpha +org.wikipedia +org.xbmc.kodi +pl.zdunex25.updater +tv.twitch.android.app +tw.com.gamer.android.activecenter +videodownloader.downloadvideo.downloader +uk.co.bbc.learningenglish +com.ted.android +de.danoeh.antennapod +com.kiwibrowser.browser +nekox.messenger +com.nextcloud.client +com.aurora.store +com.aurora.adroid +chat.simplex.app +im.vector.app +network.loki.messenger +eu.siacs.conversations +xyz.nextalone.nagram +de.danoeh.antennapod +net.programmierecke.radiodroid2 +im.fdx.v2ex +ml.docilealligator.infinityforreddit +com.bytemyth.ama +app.vanadium.browser +com.cakewallet.cake_wallet +org.purplei2p.i2pd +dk.tacit.android.foldersync.lite +com.nononsenseapps.feeder +com.m2049r.xmrwallet +com.paypal.android.p2pmobile +com.google.android.apps.googlevoice +com.readdle.spark +org.torproject.torbrowser +com.deepl.mobiletranslator +com.microsoft.bing +com.keylesspalace.tusky +com.ottplay.ottplay +ru.iptvremote.android.iptv.pro +jp.naver.line.android +com.xmflsct.app.tooot +com.forem.android +app.revanced.android.youtube +app.rvx.android.youtube +app.rvx.android.apps.youtube.music +com.mgoogle.android.gms +com.pionex.client +vip.mytokenpocket +im.token.app +com.linekong.mars24 +com.feixiaohao +com.aicoin.appandroid +com.binance.dev +com.kraken.trade +com.okinc.okex.gp +com.authy.authy +air.com.rosettastone.mobile.CoursePlayer +com.blizzard.bma +com.amazon.kindle +com.google.android.apps.fitness +net.tsapps.appsales +com.wemesh.android +com.google.android.apps.googleassistant +allen.town.focus.reader +me.hyliu.fluent_reader_lite +com.aljazeera.mobile +com.ft.news +de.marmaro.krt.ffupdater +myradio.radio.fmradio.liveradio.radiostation +com.google.earth +eu.kanade.tachiyomi.j2k +com.audials +com.microsoft.skydrive +com.mb.android.tg +com.melodis.midomiMusicIdentifier.freemium +com.foxnews.android +ch.threema.app +com.briarproject.briar.android +foundation.e.apps +com.valvesoftware.android.steam.friendsui +com.imback.yeetalk +so.onekey.app.wallet +com.xc3fff0e.xmanager +meditofoundation.medito +com.picol.client +com.streetwriters.notesnook +shanghai.panewsApp.com +org.coursera.android +com.positron_it.zlib +com.blizzard.messenger +com.javdb.javrocket +com.picacomic.fregata +com.fxl.chacha +me.proton.android.drive +com.lastpass.lpandroid +com.tradingview.tradingviewapp +com.deviantart.android.damobile +com.fusionmedia.investing +com.ewa.ewaapp +com.duolingo +com.hellotalk +io.github.huskydg.magisk +com.jsy.xpgbox +com.hostloc.app.hostloc +com.dena.pokota +com.vitorpamplona.amethyst +com.zhiliaoapp.musically +us.spotco.fennec_dos +com.fongmi.android.tv +com.pocketprep.android.itcybersecurity +com.cloudtv +com.glassdoor.app +com.indeed.android.jobsearch +com.linkedin.android +com.github.tvbox.osc.bh +com.example.douban +com.sipnetic.app +com.microsoft.rdc.androidx +org.zwanoo.android.speedtest +com.sonelli.juicessh +com.scmp.newspulse +org.lsposed.manager +mnn.Android +com.thomsonretuers.reuters +com.guardian +com.ttxapps.onesyncv2 +org.fcitx.fcitx5.android.updater +com.instagram.barcelona +com.deniscerri.ytdl +jp.pokemon.pokemonsleep +com.github.android +com.openai.chatgpt +mega.privacy.android.app +com.taptap.global +tw.com.gamer.android.animad +com.microsoft.copilot +com.google.android.apps.aiwallpapers +ai.x.grok +com.google.android.apps.weather +com.metrolist.music +com.google.android.apps.youtube.creator \ No newline at end of file diff --git a/app/src/main/assets/yacd.version.txt b/app/src/main/assets/yacd.version.txt index 56a6051ca..e440e5c84 100644 --- a/app/src/main/assets/yacd.version.txt +++ b/app/src/main/assets/yacd.version.txt @@ -1 +1 @@ -1 \ No newline at end of file +3 \ No newline at end of file diff --git a/app/src/main/assets/yacd.zip b/app/src/main/assets/yacd.zip index 46d2b59b6..5fc55e2e0 100644 Binary files a/app/src/main/assets/yacd.zip and b/app/src/main/assets/yacd.zip differ diff --git a/app/src/main/java/com/wireguard/crypto/Curve25519.java b/app/src/main/java/com/wireguard/crypto/Curve25519.java deleted file mode 100644 index 55f2809af..000000000 --- a/app/src/main/java/com/wireguard/crypto/Curve25519.java +++ /dev/null @@ -1,497 +0,0 @@ -/* - * Copyright © 2016 Southern Storm Software, Pty Ltd. - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import androidx.annotation.Nullable; - -import java.util.Arrays; - -/** - * Implementation of Curve25519 ECDH. - *

- * This implementation was imported to WireGuard from noise-java: - * https://github.com/rweather/noise-java - *

- * This implementation is based on that from arduinolibs: - * https://github.com/rweather/arduinolibs - *

- * Differences in this version are due to using 26-bit limbs for the - * representation instead of the 8/16/32-bit limbs in the original. - *

- * References: http://cr.yp.to/ecdh.html, RFC 7748 - */ -@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"}) -public final class Curve25519 { - // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words. - private static final int NUM_LIMBS_255BIT = 10; - private static final int NUM_LIMBS_510BIT = 20; - - private final int[] A; - private final int[] AA; - private final int[] B; - private final int[] BB; - private final int[] C; - private final int[] CB; - private final int[] D; - private final int[] DA; - private final int[] E; - private final long[] t1; - private final int[] t2; - private final int[] x_1; - private final int[] x_2; - private final int[] x_3; - private final int[] z_2; - private final int[] z_3; - - /** - * Constructs the temporary state holder for Curve25519 evaluation. - */ - private Curve25519() { - // Allocate memory for all of the temporary variables we will need. - x_1 = new int[NUM_LIMBS_255BIT]; - x_2 = new int[NUM_LIMBS_255BIT]; - x_3 = new int[NUM_LIMBS_255BIT]; - z_2 = new int[NUM_LIMBS_255BIT]; - z_3 = new int[NUM_LIMBS_255BIT]; - A = new int[NUM_LIMBS_255BIT]; - B = new int[NUM_LIMBS_255BIT]; - C = new int[NUM_LIMBS_255BIT]; - D = new int[NUM_LIMBS_255BIT]; - E = new int[NUM_LIMBS_255BIT]; - AA = new int[NUM_LIMBS_255BIT]; - BB = new int[NUM_LIMBS_255BIT]; - DA = new int[NUM_LIMBS_255BIT]; - CB = new int[NUM_LIMBS_255BIT]; - t1 = new long[NUM_LIMBS_510BIT]; - t2 = new int[NUM_LIMBS_510BIT]; - } - - /** - * Conditional swap of two values. - * - * @param select Set to 1 to swap, 0 to leave as-is. - * @param x The first value. - * @param y The second value. - */ - private static void cswap(int select, final int[] x, final int[] y) { - select = -select; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - final int dummy = select & (x[index] ^ y[index]); - x[index] ^= dummy; - y[index] ^= dummy; - } - } - - /** - * Evaluates the Curve25519 curve. - * - * @param result Buffer to place the result of the evaluation into. - * @param offset Offset into the result buffer. - * @param privateKey The private key to use in the evaluation. - * @param publicKey The public key to use in the evaluation, or null - * if the base point of the curve should be used. - */ - public static void eval(final byte[] result, final int offset, - final byte[] privateKey, @Nullable final byte[] publicKey) { - final Curve25519 state = new Curve25519(); - try { - // Unpack the public key value. If null, use 9 as the base point. - Arrays.fill(state.x_1, 0); - if (publicKey != null) { - // Convert the input value from little-endian into 26-bit limbs. - for (int index = 0; index < 32; ++index) { - final int bit = (index * 8) % 26; - final int word = (index * 8) / 26; - final int value = publicKey[index] & 0xFF; - if (bit <= (26 - 8)) { - state.x_1[word] |= value << bit; - } else { - state.x_1[word] |= value << bit; - state.x_1[word] &= 0x03FFFFFF; - state.x_1[word + 1] |= value >> (26 - bit); - } - } - - // Just in case, we reduce the number modulo 2^255 - 19 to - // make sure that it is in range of the field before we start. - // This eliminates values between 2^255 - 19 and 2^256 - 1. - state.reduceQuick(state.x_1); - state.reduceQuick(state.x_1); - } else { - state.x_1[0] = 9; - } - - // Initialize the other temporary variables. - Arrays.fill(state.x_2, 0); // x_2 = 1 - state.x_2[0] = 1; - Arrays.fill(state.z_2, 0); // z_2 = 0 - System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1 - Arrays.fill(state.z_3, 0); // z_3 = 1 - state.z_3[0] = 1; - - // Evaluate the curve for every bit of the private key. - state.evalCurve(privateKey); - - // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19. - state.recip(state.z_3, state.z_2); - state.mul(state.x_2, state.x_2, state.z_3); - - // Convert x_2 into little-endian in the result buffer. - for (int index = 0; index < 32; ++index) { - final int bit = (index * 8) % 26; - final int word = (index * 8) / 26; - if (bit <= (26 - 8)) - result[offset + index] = (byte) (state.x_2[word] >> bit); - else - result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit))); - } - } finally { - // Clean up all temporary state before we exit. - state.destroy(); - } - } - - /** - * Subtracts two numbers modulo 2^255 - 19. - * - * @param result The result. - * @param x The first number to subtract. - * @param y The second number to subtract. - */ - private static void sub(final int[] result, final int[] x, final int[] y) { - int index; - int borrow; - - // Subtract y from x to generate the intermediate result. - borrow = 0; - for (index = 0; index < NUM_LIMBS_255BIT; ++index) { - borrow = x[index] - y[index] - ((borrow >> 26) & 0x01); - result[index] = borrow & 0x03FFFFFF; - } - - // If we had a borrow, then the result has gone negative and we - // have to add 2^255 - 19 to the result to make it positive again. - // The top bits of "borrow" will be all 1's if there is a borrow - // or it will be all 0's if there was no borrow. Easiest is to - // conditionally subtract 19 and then mask off the high bits. - borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19); - result[0] = borrow & 0x03FFFFFF; - for (index = 1; index < NUM_LIMBS_255BIT; ++index) { - borrow = result[index] - ((borrow >> 26) & 0x01); - result[index] = borrow & 0x03FFFFFF; - } - result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - } - - /** - * Adds two numbers modulo 2^255 - 19. - * - * @param result The result. - * @param x The first number to add. - * @param y The second number to add. - */ - private void add(final int[] result, final int[] x, final int[] y) { - int carry = x[0] + y[0]; - result[0] = carry & 0x03FFFFFF; - for (int index = 1; index < NUM_LIMBS_255BIT; ++index) { - carry = (carry >> 26) + x[index] + y[index]; - result[index] = carry & 0x03FFFFFF; - } - reduceQuick(result); - } - - /** - * Destroy all sensitive data in this object. - */ - private void destroy() { - // Destroy all temporary variables. - Arrays.fill(x_1, 0); - Arrays.fill(x_2, 0); - Arrays.fill(x_3, 0); - Arrays.fill(z_2, 0); - Arrays.fill(z_3, 0); - Arrays.fill(A, 0); - Arrays.fill(B, 0); - Arrays.fill(C, 0); - Arrays.fill(D, 0); - Arrays.fill(E, 0); - Arrays.fill(AA, 0); - Arrays.fill(BB, 0); - Arrays.fill(DA, 0); - Arrays.fill(CB, 0); - Arrays.fill(t1, 0L); - Arrays.fill(t2, 0); - } - - /** - * Evaluates the curve for every bit in a secret key. - * - * @param s The 32-byte secret key. - */ - private void evalCurve(final byte[] s) { - int sposn = 31; - int sbit = 6; - int svalue = s[sposn] | 0x40; - int swap = 0; - - // Iterate over all 255 bits of "s" from the highest to the lowest. - // We ignore the high bit of the 256-bit representation of "s". - while (true) { - // Conditional swaps on entry to this bit but only if we - // didn't swap on the previous bit. - final int select = (svalue >> sbit) & 0x01; - swap ^= select; - cswap(swap, x_2, x_3); - cswap(swap, z_2, z_3); - swap = select; - - // Evaluate the curve. - add(A, x_2, z_2); // A = x_2 + z_2 - square(AA, A); // AA = A^2 - sub(B, x_2, z_2); // B = x_2 - z_2 - square(BB, B); // BB = B^2 - sub(E, AA, BB); // E = AA - BB - add(C, x_3, z_3); // C = x_3 + z_3 - sub(D, x_3, z_3); // D = x_3 - z_3 - mul(DA, D, A); // DA = D * A - mul(CB, C, B); // CB = C * B - add(x_3, DA, CB); // x_3 = (DA + CB)^2 - square(x_3, x_3); - sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2 - square(z_3, z_3); - mul(z_3, z_3, x_1); - mul(x_2, AA, BB); // x_2 = AA * BB - mulA24(z_2, E); // z_2 = E * (AA + a24 * E) - add(z_2, z_2, AA); - mul(z_2, z_2, E); - - // Move onto the next lower bit of "s". - if (sbit > 0) { - --sbit; - } else if (sposn == 0) { - break; - } else if (sposn == 1) { - --sposn; - svalue = s[sposn] & 0xF8; - sbit = 7; - } else { - --sposn; - svalue = s[sposn]; - sbit = 7; - } - } - - // Final conditional swaps. - cswap(swap, x_2, x_3); - cswap(swap, z_2, z_3); - } - - /** - * Multiplies two numbers modulo 2^255 - 19. - * - * @param result The result. - * @param x The first number to multiply. - * @param y The second number to multiply. - */ - private void mul(final int[] result, final int[] x, final int[] y) { - // Multiply the two numbers to create the intermediate result. - long v = x[0]; - for (int i = 0; i < NUM_LIMBS_255BIT; ++i) { - t1[i] = v * y[i]; - } - for (int i = 1; i < NUM_LIMBS_255BIT; ++i) { - v = x[i]; - for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) { - t1[i + j] += v * y[j]; - } - t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1]; - } - - // Propagate carries and convert back into 26-bit words. - v = t1[0]; - t2[0] = ((int) v) & 0x03FFFFFF; - for (int i = 1; i < NUM_LIMBS_510BIT; ++i) { - v = (v >> 26) + t1[i]; - t2[i] = ((int) v) & 0x03FFFFFF; - } - - // Reduce the result modulo 2^255 - 19. - reduce(result, t2, NUM_LIMBS_255BIT); - } - - /** - * Multiplies a number by the a24 constant, modulo 2^255 - 19. - * - * @param result The result. - * @param x The number to multiply by a24. - */ - private void mulA24(final int[] result, final int[] x) { - final long a24 = 121665; - long carry = 0; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - carry += a24 * x[index]; - t2[index] = ((int) carry) & 0x03FFFFFF; - carry >>= 26; - } - t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF; - reduce(result, t2, 1); - } - - /** - * Raise x to the power of (2^250 - 1). - * - * @param result The result. Must not overlap with x. - * @param x The argument. - */ - private void pow250(final int[] result, final int[] x) { - // The big-endian hexadecimal expansion of (2^250 - 1) is: - // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF - // - // The naive implementation needs to do 2 multiplications per 1 bit and - // 1 multiplication per 0 bit. We can improve upon this by creating a - // pattern 0000000001 ... 0000000001. If we square and multiply the - // pattern by itself we can turn the pattern into the partial results - // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc. - // This averages out to about 1.1 multiplications per 1 bit instead of 2. - - // Build a pattern of 250 bits in length of repeated copies of 0000000001. - square(A, x); - for (int j = 0; j < 9; ++j) - square(A, A); - mul(result, A, x); - for (int i = 0; i < 23; ++i) { - for (int j = 0; j < 10; ++j) - square(A, A); - mul(result, result, A); - } - - // Multiply bit-shifted versions of the 0000000001 pattern into - // the result to "fill in" the gaps in the pattern. - square(A, result); - mul(result, result, A); - for (int j = 0; j < 8; ++j) { - square(A, A); - mul(result, result, A); - } - } - - /** - * Computes the reciprocal of a number modulo 2^255 - 19. - * - * @param result The result. Must not overlap with x. - * @param x The argument. - */ - private void recip(final int[] result, final int[] x) { - // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19. - // The big-endian hexadecimal expansion of (p - 2) is: - // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB - // Start with the 250 upper bits of the expansion of (p - 2). - pow250(result, x); - - // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest. - square(result, result); - square(result, result); - mul(result, result, x); - square(result, result); - square(result, result); - mul(result, result, x); - square(result, result); - mul(result, result, x); - } - - /** - * Reduce a number modulo 2^255 - 19. - * - * @param result The result. - * @param x The value to be reduced. This array will be - * modified during the reduction. - * @param size The number of limbs in the high order half of x. - */ - private void reduce(final int[] result, final int[] x, final int size) { - // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will - // either produce the answer we want or it will produce a - // value of the form "answer + j * (2^255 - 19)". There are - // 5 left-over bits in the top-most limb of the bottom half. - int carry = 0; - int limb = x[NUM_LIMBS_255BIT - 1] >> 21; - x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - for (int index = 0; index < size; ++index) { - limb += x[NUM_LIMBS_255BIT + index] << 5; - carry += (limb & 0x03FFFFFF) * 19 + x[index]; - x[index] = carry & 0x03FFFFFF; - limb >>= 26; - carry >>= 26; - } - if (size < NUM_LIMBS_255BIT) { - // The high order half of the number is short; e.g. for mulA24(). - // Propagate the carry through the rest of the low order part. - for (int index = size; index < NUM_LIMBS_255BIT; ++index) { - carry += x[index]; - x[index] = carry & 0x03FFFFFF; - carry >>= 26; - } - } - - // The "j" value may still be too large due to the final carry-out. - // We must repeat the reduction. If we already have the answer, - // then this won't do any harm but we must still do the calculation - // to preserve the overall timing. The "j" value will be between - // 0 and 19, which means that the carry we care about is in the - // top 5 bits of the highest limb of the bottom half. - carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19; - x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - carry += x[index]; - result[index] = carry & 0x03FFFFFF; - carry >>= 26; - } - - // At this point "x" will either be the answer or it will be the - // answer plus (2^255 - 19). Perform a trial subtraction to - // complete the reduction process. - reduceQuick(result); - } - - /** - * Reduces a number modulo 2^255 - 19 where it is known that the - * number can be reduced with only 1 trial subtraction. - * - * @param x The number to reduce, and the result. - */ - private void reduceQuick(final int[] x) { - // Perform a trial subtraction of (2^255 - 19) from "x" which is - // equivalent to adding 19 and subtracting 2^255. We add 19 here; - // the subtraction of 2^255 occurs in the next step. - int carry = 19; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - carry += x[index]; - t2[index] = carry & 0x03FFFFFF; - carry >>= 26; - } - - // If there was a borrow, then the original "x" is the correct answer. - // If there was no borrow, then "t2" is the correct answer. Select the - // correct answer but do it in a way that instruction timing will not - // reveal which value was selected. Borrow will occur if bit 21 of - // "t2" is zero. Turn the bit into a selection mask. - final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01); - final int nmask = ~mask; - t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) - x[index] = (x[index] & nmask) | (t2[index] & mask); - } - - /** - * Squares a number modulo 2^255 - 19. - * - * @param result The result. - * @param x The number to square. - */ - private void square(final int[] result, final int[] x) { - mul(result, x, x); - } -} diff --git a/app/src/main/java/com/wireguard/crypto/Ed25519.java b/app/src/main/java/com/wireguard/crypto/Ed25519.java deleted file mode 100644 index a60babfbb..000000000 --- a/app/src/main/java/com/wireguard/crypto/Ed25519.java +++ /dev/null @@ -1,2508 +0,0 @@ -/* - * Copyright © 2020 WireGuard LLC. All Rights Reserved. - * Copyright 2017 Google Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.util.Arrays; - -/** - * Implementation of Ed25519 signature verification. - * - *

This implementation is based on the ed25519/ref10 implementation in NaCl.

- * - *

It implements this twisted Edwards curve: - * - *

- * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
- * 
- * - * @see Bernstein D.J., Birkner P., Joye M., Lange - * T., Peters C. (2008) Twisted Edwards Curves - * @see Hisil H., Wong K.KH., Carter G., Dawson E. - * (2008) Twisted Edwards Curves Revisited - */ -public final class Ed25519 { - - // d = -121665 / 121666 mod 2^255-19 - private static final long[] D; - // 2d - private static final long[] D2; - // 2^((p-1)/4) mod p where p = 2^255-19 - private static final long[] SQRTM1; - - /** - * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] = - * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0] - */ - private static final CachedXYT[][] B_TABLE; - private static final CachedXYT[] B2; - - private static final BigInteger P_BI = - BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19)); - private static final BigInteger D_BI = - BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI); - private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI); - private static final BigInteger SQRTM1_BI = - BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI); - - private Ed25519() { - } - - private static class Point { - private BigInteger x; - private BigInteger y; - } - - private static BigInteger recoverX(BigInteger y) { - // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19 - BigInteger xx = - y.pow(2) - .subtract(BigInteger.ONE) - .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI)); - BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI); - if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) { - x = x.multiply(SQRTM1_BI).mod(P_BI); - } - if (x.testBit(0)) { - x = P_BI.subtract(x); - } - return x; - } - - private static Point edwards(Point a, Point b) { - Point o = new Point(); - BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI); - o.x = - (a.x.multiply(b.y).add(b.x.multiply(a.y))) - .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI)) - .mod(P_BI); - o.y = - (a.y.multiply(b.y).add(a.x.multiply(b.x))) - .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI)) - .mod(P_BI); - return o; - } - - private static byte[] toLittleEndian(BigInteger n) { - byte[] b = new byte[32]; - byte[] nBytes = n.toByteArray(); - System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length); - for (int i = 0; i < b.length / 2; i++) { - byte t = b[i]; - b[i] = b[b.length - i - 1]; - b[b.length - i - 1] = t; - } - return b; - } - - private static CachedXYT getCachedXYT(Point p) { - return new CachedXYT( - Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))), - Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))), - Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI)))); - } - - static { - Point b = new Point(); - b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI); - b.x = recoverX(b.y); - - D = Field25519.expand(toLittleEndian(D_BI)); - D2 = Field25519.expand(toLittleEndian(D2_BI)); - SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI)); - - Point bi = b; - B_TABLE = new CachedXYT[32][8]; - for (int i = 0; i < 32; i++) { - Point bij = bi; - for (int j = 0; j < 8; j++) { - B_TABLE[i][j] = getCachedXYT(bij); - bij = edwards(bij, bi); - } - for (int j = 0; j < 8; j++) { - bi = edwards(bi, bi); - } - } - bi = b; - Point b2 = edwards(b, b); - B2 = new CachedXYT[8]; - for (int i = 0; i < 8; i++) { - B2[i] = getCachedXYT(bi); - bi = edwards(bi, b2); - } - } - - private static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN; - private static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2; - - /** - * Defines field 25519 function based on curve25519-donna C - * implementation (mostly identical). - * - *

Field elements are written as an array of signed, 64-bit limbs (an array of longs), least - * significant first. The value of the field element is: - * - *

-     * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
-     * 2^204·x[8] + 2^230·x[9],
-     * 
- * - *

i.e. the limbs are 26, 25, 26, 25, ... bits wide. - */ - private static final class Field25519 { - /** - * During Field25519 computation, the mixed radix representation may be in different forms: - *

    - *
  • Reduced-size form: the array has size at most 10. - *
  • Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most - * 19. - *
- *

- * TODO(quannguyen): - *

    - *
  • Clarify ill-defined terminologies. - *
  • The reduction procedure is different from DJB's paper - * (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and - * reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to - * see what's going on. - *
  • Consider using method mult() everywhere and making product() private. - *
- */ - - static final int FIELD_LEN = 32; - static final int LIMB_CNT = 10; - private static final long TWO_TO_25 = 1 << 25; - private static final long TWO_TO_26 = TWO_TO_25 << 1; - - private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28}; - private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6}; - private static final int[] MASK = {0x3ffffff, 0x1ffffff}; - private static final int[] SHIFT = {26, 25}; - - /** - * Sums two numbers: output = in1 + in2 - *

- * On entry: in1, in2 are in reduced-size form. - */ - static void sum(long[] output, long[] in1, long[] in2) { - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = in1[i] + in2[i]; - } - } - - /** - * Sums two numbers: output += in - *

- * On entry: in is in reduced-size form. - */ - static void sum(long[] output, long[] in) { - sum(output, output, in); - } - - /** - * Find the difference of two numbers: output = in1 - in2 - * (note the order of the arguments!). - *

- * On entry: in1, in2 are in reduced-size form. - */ - static void sub(long[] output, long[] in1, long[] in2) { - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = in1[i] - in2[i]; - } - } - - /** - * Find the difference of two numbers: output = in - output - * (note the order of the arguments!). - *

- * On entry: in, output are in reduced-size form. - */ - static void sub(long[] output, long[] in) { - sub(output, in, output); - } - - /** - * Multiply a number by a scalar: output = in * scalar - */ - static void scalarProduct(long[] output, long[] in, long scalar) { - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = in[i] * scalar; - } - } - - /** - * Multiply two numbers: out = in2 * in - *

- * output must be distinct to both inputs. The inputs are reduced coefficient form, - * the output is not. - *

- * out[x] <= 14 * the largest product of the input limbs. - */ - static void product(long[] out, long[] in2, long[] in) { - out[0] = in2[0] * in[0]; - out[1] = in2[0] * in[1] - + in2[1] * in[0]; - out[2] = 2 * in2[1] * in[1] - + in2[0] * in[2] - + in2[2] * in[0]; - out[3] = in2[1] * in[2] - + in2[2] * in[1] - + in2[0] * in[3] - + in2[3] * in[0]; - out[4] = in2[2] * in[2] - + 2 * (in2[1] * in[3] + in2[3] * in[1]) - + in2[0] * in[4] - + in2[4] * in[0]; - out[5] = in2[2] * in[3] - + in2[3] * in[2] - + in2[1] * in[4] - + in2[4] * in[1] - + in2[0] * in[5] - + in2[5] * in[0]; - out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1]) - + in2[2] * in[4] - + in2[4] * in[2] - + in2[0] * in[6] - + in2[6] * in[0]; - out[7] = in2[3] * in[4] - + in2[4] * in[3] - + in2[2] * in[5] - + in2[5] * in[2] - + in2[1] * in[6] - + in2[6] * in[1] - + in2[0] * in[7] - + in2[7] * in[0]; - out[8] = in2[4] * in[4] - + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1]) - + in2[2] * in[6] - + in2[6] * in[2] - + in2[0] * in[8] - + in2[8] * in[0]; - out[9] = in2[4] * in[5] - + in2[5] * in[4] - + in2[3] * in[6] - + in2[6] * in[3] - + in2[2] * in[7] - + in2[7] * in[2] - + in2[1] * in[8] - + in2[8] * in[1] - + in2[0] * in[9] - + in2[9] * in[0]; - out[10] = - 2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1]) - + in2[4] * in[6] - + in2[6] * in[4] - + in2[2] * in[8] - + in2[8] * in[2]; - out[11] = in2[5] * in[6] - + in2[6] * in[5] - + in2[4] * in[7] - + in2[7] * in[4] - + in2[3] * in[8] - + in2[8] * in[3] - + in2[2] * in[9] - + in2[9] * in[2]; - out[12] = in2[6] * in[6] - + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3]) - + in2[4] * in[8] - + in2[8] * in[4]; - out[13] = in2[6] * in[7] - + in2[7] * in[6] - + in2[5] * in[8] - + in2[8] * in[5] - + in2[4] * in[9] - + in2[9] * in[4]; - out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5]) - + in2[6] * in[8] - + in2[8] * in[6]; - out[15] = in2[7] * in[8] - + in2[8] * in[7] - + in2[6] * in[9] - + in2[9] * in[6]; - out[16] = in2[8] * in[8] - + 2 * (in2[7] * in[9] + in2[9] * in[7]); - out[17] = in2[8] * in[9] - + in2[9] * in[8]; - out[18] = 2 * in2[9] * in[9]; - } - - /** - * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients. - * - * @param input An input array of any length. If the array has 19 elements, it will be used as - * temporary buffer and its contents changed. - * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold. - */ - static void reduce(long[] input, long[] output) { - long[] tmp; - if (input.length == 19) { - tmp = input; - } else { - tmp = new long[19]; - System.arraycopy(input, 0, tmp, 0, input.length); - } - reduceSizeByModularReduction(tmp); - reduceCoefficients(tmp); - System.arraycopy(tmp, 0, output, 0, LIMB_CNT); - } - - /** - * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19. - *

- * On entry: |output[i]| < 14*2^54 - * On exit: |output[0..8]| < 280*2^54 - */ - static void reduceSizeByModularReduction(long[] output) { - // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19. - // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8]. - // - // Each of these shifts and adds ends up multiplying the value by 19. - // - // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus, - // on exit, |output[0..8]| < 280*2^54. - output[8] += output[18] << 4; - output[8] += output[18] << 1; - output[8] += output[18]; - output[7] += output[17] << 4; - output[7] += output[17] << 1; - output[7] += output[17]; - output[6] += output[16] << 4; - output[6] += output[16] << 1; - output[6] += output[16]; - output[5] += output[15] << 4; - output[5] += output[15] << 1; - output[5] += output[15]; - output[4] += output[14] << 4; - output[4] += output[14] << 1; - output[4] += output[14]; - output[3] += output[13] << 4; - output[3] += output[13] << 1; - output[3] += output[13]; - output[2] += output[12] << 4; - output[2] += output[12] << 1; - output[2] += output[12]; - output[1] += output[11] << 4; - output[1] += output[11] << 1; - output[1] += output[11]; - output[0] += output[10] << 4; - output[0] += output[10] << 1; - output[0] += output[10]; - } - - /** - * Reduce all coefficients of the short form input so that |x| < 2^26. - *

- * On entry: |output[i]| < 280*2^54 - */ - static void reduceCoefficients(long[] output) { - output[10] = 0; - - for (int i = 0; i < LIMB_CNT; i += 2) { - long over = output[i] / TWO_TO_26; - // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in - // the first iteration of this loop. This is added to the next limb and we can approximate the - // resulting bound of that limb by 281*2^54. - output[i] -= over << 26; - output[i + 1] += over; - - // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is - // added to the next limb, the resulting bound can be approximated as 281*2^54. - // - // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no - // overflow occurs. - over = output[i + 1] / TWO_TO_25; - output[i + 1] -= over << 25; - output[i + 2] += over; - } - // Now |output[10]| < 281*2^29 and all other coefficients are reduced. - output[0] += output[10] << 4; - output[0] += output[10] << 1; - output[0] += output[10]; - - output[10] = 0; - // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more - // than 2^16. - long over = output[0] / TWO_TO_26; - output[0] -= over << 26; - output[1] += over; - // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on - // |output[1]| is sufficient to meet our needs. - } - - /** - * A helpful wrapper around {@ref Field25519#product}: output = in * in2. - *

- * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27. - *

- * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and - * |output[i]| < 2^26. - */ - static void mult(long[] output, long[] in, long[] in2) { - long[] t = new long[19]; - product(t, in, in2); - // |t[i]| < 2^26 - reduce(t, output); - } - - /** - * Square a number: out = in**2 - *

- * output must be distinct from the input. The inputs are reduced coefficient form, the output is - * not. - *

- * out[x] <= 14 * the largest product of the input limbs. - */ - private static void squareInner(long[] out, long[] in) { - out[0] = in[0] * in[0]; - out[1] = 2 * in[0] * in[1]; - out[2] = 2 * (in[1] * in[1] + in[0] * in[2]); - out[3] = 2 * (in[1] * in[2] + in[0] * in[3]); - out[4] = in[2] * in[2] - + 4 * in[1] * in[3] - + 2 * in[0] * in[4]; - out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]); - out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]); - out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]); - out[8] = in[4] * in[4] - + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5])); - out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]); - out[10] = 2 * (in[5] * in[5] - + in[4] * in[6] - + in[2] * in[8] - + 2 * (in[3] * in[7] + in[1] * in[9])); - out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]); - out[12] = in[6] * in[6] - + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9])); - out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]); - out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]); - out[15] = 2 * (in[7] * in[8] + in[6] * in[9]); - out[16] = in[8] * in[8] + 4 * in[7] * in[9]; - out[17] = 2 * in[8] * in[9]; - out[18] = 2 * in[9] * in[9]; - } - - /** - * Returns in^2. - *

- * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27. - *

- * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide - * storage for 10 limbs) and |out[i]| < 2^26. - */ - static void square(long[] output, long[] in) { - long[] t = new long[19]; - squareInner(t, in); - // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner - // adds together, at most, 14 of those products. - reduce(t, output); - } - - /** - * Takes a little-endian, 32-byte number and expands it into mixed radix form. - */ - static long[] expand(byte[] input) { - long[] output = new long[LIMB_CNT]; - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = ((((long) (input[EXPAND_START[i]] & 0xff)) - | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8 - | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16 - | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1]; - } - return output; - } - - /** - * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte - * array. - *

- * On entry: |input_limbs[i]| < 2^26 - */ - @SuppressWarnings("NarrowingCompoundAssignment") - static byte[] contract(long[] inputLimbs) { - long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT); - for (int j = 0; j < 2; j++) { - for (int i = 0; i < 9; i++) { - // This calculation is a time-invariant way to make input[i] non-negative by borrowing - // from the next-larger limb. - int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]); - input[i] = input[i] + (carry << SHIFT[i & 1]); - input[i + 1] -= carry; - } - - // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow - // from input[0], which is valid mod 2^255-19. - { - int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25); - input[9] += (carry << 25); - input[0] -= (carry * 19); - } - - // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits, - // depending on position. However, input[0] may be negative. - } - - // The first borrow-propagation pass above ended with every limb except (possibly) input[0] - // non-negative. - // - // If input[0] was negative after the first pass, then it was because of a carry from input[9]. - // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus - // input[0] >= -19. - // - // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation - // pass could only have wrapped around to decrease input[0] again if the first pass left - // input[0] negative *and* input[1] through input[9] were all zero. In that case, input[1] is - // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative. - { - int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26); - input[0] += (carry << 26); - input[1] -= carry; - } - - // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a - // limb which is, nominally, 25 bits wide. - for (int j = 0; j < 2; j++) { - for (int i = 0; i < 9; i++) { - int carry = (int) (input[i] >> SHIFT[i & 1]); - input[i] &= MASK[i & 1]; - input[i + 1] += carry; - } - } - - { - int carry = (int) (input[9] >> 25); - input[9] &= 0x1ffffff; - input[0] += 19 * carry; - } - - // If the first carry-chain pass, just above, ended up with a carry from input[9], and that - // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was, - // at most, two. - // - // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] -> - // input[0] carry didn't push input[0] out of bounds. - - // It still remains the case that input might be between 2^255-19 and 2^255. In this case, - // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff, - // which is 0x3ffffed. - int mask = gte((int) input[0], 0x3ffffed); - for (int i = 1; i < LIMB_CNT; i++) { - mask &= eq((int) input[i], MASK[i & 1]); - } - - // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally - // subtracts 2^255-19. - input[0] -= mask & 0x3ffffed; - input[1] -= mask & 0x1ffffff; - for (int i = 2; i < LIMB_CNT; i += 2) { - input[i] -= mask & 0x3ffffff; - input[i + 1] -= mask & 0x1ffffff; - } - - for (int i = 0; i < LIMB_CNT; i++) { - input[i] <<= EXPAND_SHIFT[i]; - } - byte[] output = new byte[FIELD_LEN]; - for (int i = 0; i < LIMB_CNT; i++) { - output[EXPAND_START[i]] |= input[i] & 0xff; - output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff; - output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff; - output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff; - } - return output; - } - - /** - * Computes inverse of z = z(2^255 - 21) - *

- * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the - * comment format and the variable namings are different from those. - */ - static void inverse(long[] out, long[] z) { - long[] z2 = new long[Field25519.LIMB_CNT]; - long[] z9 = new long[Field25519.LIMB_CNT]; - long[] z11 = new long[Field25519.LIMB_CNT]; - long[] z2To5Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To10Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To20Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To50Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To100Minus1 = new long[Field25519.LIMB_CNT]; - long[] t0 = new long[Field25519.LIMB_CNT]; - long[] t1 = new long[Field25519.LIMB_CNT]; - - square(z2, z); // 2 - square(t1, z2); // 4 - square(t0, t1); // 8 - mult(z9, t0, z); // 9 - mult(z11, z9, z2); // 11 - square(t0, z11); // 22 - mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31 - - square(t0, z2To5Minus1); // 2^6 - 2^1 - square(t1, t0); // 2^7 - 2^2 - square(t0, t1); // 2^8 - 2^3 - square(t1, t0); // 2^9 - 2^4 - square(t0, t1); // 2^10 - 2^5 - mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0 - - square(t0, z2To10Minus1); // 2^11 - 2^1 - square(t1, t0); // 2^12 - 2^2 - for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10 - square(t0, t1); - square(t1, t0); - } - mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0 - - square(t0, z2To20Minus1); // 2^21 - 2^1 - square(t1, t0); // 2^22 - 2^2 - for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20 - square(t0, t1); - square(t1, t0); - } - mult(t0, t1, z2To20Minus1); // 2^40 - 2^0 - - square(t1, t0); // 2^41 - 2^1 - square(t0, t1); // 2^42 - 2^2 - for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10 - square(t1, t0); - square(t0, t1); - } - mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0 - - square(t0, z2To50Minus1); // 2^51 - 2^1 - square(t1, t0); // 2^52 - 2^2 - for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50 - square(t0, t1); - square(t1, t0); - } - mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0 - - square(t1, z2To100Minus1); // 2^101 - 2^1 - square(t0, t1); // 2^102 - 2^2 - for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100 - square(t1, t0); - square(t0, t1); - } - mult(t1, t0, z2To100Minus1); // 2^200 - 2^0 - - square(t0, t1); // 2^201 - 2^1 - square(t1, t0); // 2^202 - 2^2 - for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50 - square(t0, t1); - square(t1, t0); - } - mult(t0, t1, z2To50Minus1); // 2^250 - 2^0 - - square(t1, t0); // 2^251 - 2^1 - square(t0, t1); // 2^252 - 2^2 - square(t1, t0); // 2^253 - 2^3 - square(t0, t1); // 2^254 - 2^4 - square(t1, t0); // 2^255 - 2^5 - mult(out, t1, z11); // 2^255 - 21 - } - - - /** - * Returns 0xffffffff iff a == b and zero otherwise. - */ - private static int eq(int a, int b) { - a = ~(a ^ b); - a &= a << 16; - a &= a << 8; - a &= a << 4; - a &= a << 2; - a &= a << 1; - return a >> 31; - } - - /** - * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative. - */ - private static int gte(int a, int b) { - a -= b; - // a >= 0 iff a >= b. - return ~(a >> 31); - } - } - - // (x = 0, y = 1) point - private static final CachedXYT CACHED_NEUTRAL = new CachedXYT( - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); - private static final PartialXYZT NEUTRAL = new PartialXYZT( - new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}), - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}); - - /** - * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z - *

- * Note that this is referred as ge_p2 in ref10 impl. - * Also note that x = X, y = Y and z = Z below following Java coding style. - *

- * See - * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary - * Window Method. - *

- * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html - */ - private static class XYZ { - - final long[] x; - final long[] y; - final long[] z; - - XYZ() { - this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]); - } - - XYZ(long[] x, long[] y, long[] z) { - this.x = x; - this.y = y; - this.z = z; - } - - XYZ(XYZ xyz) { - x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT); - y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT); - z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT); - } - - XYZ(PartialXYZT partialXYZT) { - this(); - fromPartialXYZT(this, partialXYZT); - } - - /** - * ge_p1p1_to_p2.c - */ - static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) { - Field25519.mult(out.x, in.xyz.x, in.t); - Field25519.mult(out.y, in.xyz.y, in.xyz.z); - Field25519.mult(out.z, in.xyz.z, in.t); - return out; - } - - /** - * Encodes this point to bytes. - */ - byte[] toBytes() { - long[] recip = new long[Field25519.LIMB_CNT]; - long[] x = new long[Field25519.LIMB_CNT]; - long[] y = new long[Field25519.LIMB_CNT]; - Field25519.inverse(recip, z); - Field25519.mult(x, this.x, recip); - Field25519.mult(y, this.y, recip); - byte[] s = Field25519.contract(y); - s[31] = (byte) (s[31] ^ (getLsb(x) << 7)); - return s; - } - - - /** - * Best effort fix-timing array comparison. - * - * @return true if two arrays are equal. - */ - private static boolean bytesEqual(final byte[] x, final byte[] y) { - if (x == null || y == null) { - return false; - } - if (x.length != y.length) { - return false; - } - int res = 0; - for (int i = 0; i < x.length; i++) { - res |= x[i] ^ y[i]; - } - return res == 0; - } - - /** - * Checks that the point is on curve - */ - boolean isOnCurve() { - long[] x2 = new long[Field25519.LIMB_CNT]; - Field25519.square(x2, x); - long[] y2 = new long[Field25519.LIMB_CNT]; - Field25519.square(y2, y); - long[] z2 = new long[Field25519.LIMB_CNT]; - Field25519.square(z2, z); - long[] z4 = new long[Field25519.LIMB_CNT]; - Field25519.square(z4, z2); - long[] lhs = new long[Field25519.LIMB_CNT]; - // lhs = y^2 - x^2 - Field25519.sub(lhs, y2, x2); - // lhs = z^2 * (y2 - x2) - Field25519.mult(lhs, lhs, z2); - long[] rhs = new long[Field25519.LIMB_CNT]; - // rhs = x^2 * y^2 - Field25519.mult(rhs, x2, y2); - // rhs = D * x^2 * y^2 - Field25519.mult(rhs, rhs, D); - // rhs = z^4 + D * x^2 * y^2 - Field25519.sum(rhs, z4); - // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually - // reduce it here. - Field25519.reduce(rhs, rhs); - // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2 - return bytesEqual(Field25519.contract(lhs), Field25519.contract(rhs)); - } - } - - /** - * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z, - * XY = ZT - *

- * Note that this is referred as ge_p3 in ref10 impl. - * Also note that t = T below following Java coding style. - *

- * See - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html - */ - private static class XYZT { - - final XYZ xyz; - final long[] t; - - XYZT() { - this(new XYZ(), new long[Field25519.LIMB_CNT]); - } - - XYZT(XYZ xyz, long[] t) { - this.xyz = xyz; - this.t = t; - } - - XYZT(PartialXYZT partialXYZT) { - this(); - fromPartialXYZT(this, partialXYZT); - } - - /** - * ge_p1p1_to_p2.c - */ - private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) { - Field25519.mult(out.xyz.x, in.xyz.x, in.t); - Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z); - Field25519.mult(out.xyz.z, in.xyz.z, in.t); - Field25519.mult(out.t, in.xyz.x, in.xyz.y); - return out; - } - - /** - * Decodes {@code s} into an extented projective point. - * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3 - */ - private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException { - long[] x = new long[Field25519.LIMB_CNT]; - long[] y = Field25519.expand(s); - long[] z = new long[Field25519.LIMB_CNT]; - z[0] = 1; - long[] t = new long[Field25519.LIMB_CNT]; - long[] u = new long[Field25519.LIMB_CNT]; - long[] v = new long[Field25519.LIMB_CNT]; - long[] vxx = new long[Field25519.LIMB_CNT]; - long[] check = new long[Field25519.LIMB_CNT]; - Field25519.square(u, y); - Field25519.mult(v, u, D); - Field25519.sub(u, u, z); // u = y^2 - 1 - Field25519.sum(v, v, z); // v = dy^2 + 1 - - long[] v3 = new long[Field25519.LIMB_CNT]; - Field25519.square(v3, v); - Field25519.mult(v3, v3, v); // v3 = v^3 - Field25519.square(x, v3); - Field25519.mult(x, x, v); - Field25519.mult(x, x, u); // x = uv^7 - - pow2252m3(x, x); // x = (uv^7)^((q-5)/8) - Field25519.mult(x, x, v3); - Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8) - - Field25519.square(vxx, x); - Field25519.mult(vxx, vxx, v); - Field25519.sub(check, vxx, u); // vx^2-u - if (isNonZeroVarTime(check)) { - Field25519.sum(check, vxx, u); // vx^2+u - if (isNonZeroVarTime(check)) { - throw new GeneralSecurityException("Cannot convert given bytes to extended projective " - + "coordinates. No square root exists for modulo 2^255-19"); - } - Field25519.mult(x, x, SQRTM1); - } - - if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) { - throw new GeneralSecurityException("Cannot convert given bytes to extended projective " - + "coordinates. Computed x is zero and encoded x's least significant bit is not zero"); - } - if (getLsb(x) == ((s[31] & 0xff) >> 7)) { - neg(x, x); - } - - Field25519.mult(t, x, y); - return new XYZT(new XYZ(x, y, z), t); - } - } - - /** - * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T - *

- * Note that this is referred as complete form in the original ref10 impl (ge_p1p1). - * Also note that t = T below following Java coding style. - *

- * Although this has the same types as XYZT, it is redefined to have its own type so that it is - * readable and 1:1 corresponds to ref10 impl. - *

- * Can be converted to XYZT as follows: - * X1 = X * T = x * Z * T = x * Z1 - * Y1 = Y * Z = y * T * Z = y * Z1 - * Z1 = Z * T = Z * T - * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1 - */ - private static class PartialXYZT { - - final XYZ xyz; - final long[] t; - - PartialXYZT() { - this(new XYZ(), new long[Field25519.LIMB_CNT]); - } - - PartialXYZT(XYZ xyz, long[] t) { - this.xyz = xyz; - this.t = t; - } - - PartialXYZT(PartialXYZT other) { - xyz = new XYZ(other.xyz); - t = Arrays.copyOf(other.t, Field25519.LIMB_CNT); - } - } - - /** - * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - * with Z = 1. - */ - private static class CachedXYT { - - final long[] yPlusX; - final long[] yMinusX; - final long[] t2d; - - /** - * Creates a cached XYZT with Z = 1 - * - * @param yPlusX y + x - * @param yMinusX y - x - * @param t2d 2d * xy - */ - CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) { - this.yPlusX = yPlusX; - this.yMinusX = yMinusX; - this.t2d = t2d; - } - - CachedXYT(CachedXYT other) { - yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT); - yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT); - t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT); - } - - // z is one implicitly, so this just copies {@code in} to {@code output}. - void multByZ(long[] output, long[] in) { - System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT); - } - - /** - * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value. - */ - void copyConditional(CachedXYT other, int icopy) { - copyConditional(yPlusX, other.yPlusX, icopy); - copyConditional(yMinusX, other.yMinusX, icopy); - copyConditional(t2d, other.t2d, icopy); - } - - /** - * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1, - * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid - * side-channel attacks. - * - *

NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong - * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the - * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must - * have magnitude less than Integer.MAX_VALUE. - */ - static void copyConditional(long[] a, long[] b, int icopy) { - int copy = -icopy; - for (int i = 0; i < Field25519.LIMB_CNT; i++) { - int x = copy & (((int) a[i]) ^ ((int) b[i])); - a[i] = ((int) a[i]) ^ x; - } - } - } - - private static class CachedXYZT extends CachedXYT { - - private final long[] z; - - CachedXYZT() { - this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]); - } - - /** - * ge_p3_to_cached.c - */ - CachedXYZT(XYZT xyzt) { - this(); - Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x); - Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x); - System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT); - Field25519.mult(t2d, xyzt.t, D2); - } - - /** - * Creates a cached XYZT - * - * @param yPlusX Y + X - * @param yMinusX Y - X - * @param z Z - * @param t2d 2d * (XY/Z) - */ - CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) { - super(yPlusX, yMinusX, t2d); - this.z = z; - } - - @Override - public void multByZ(long[] output, long[] in) { - Field25519.mult(output, in, z); - } - } - - /** - * Addition defined in Section 3.1 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * Please note that this is a partial of the operation listed there leaving out the final - * conversion from PartialXYZT to XYZT. - * - * @param extended extended projective point input - * @param cached cached projective point input - */ - private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) { - long[] t = new long[Field25519.LIMB_CNT]; - - // Y1 + X1 - Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x); - - // Y1 - X1 - Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x); - - // A = (Y1 - X1) * (Y2 - X2) - Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX); - - // B = (Y1 + X1) * (Y2 + X2) - Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX); - - // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper) - Field25519.mult(partialXYZT.t, extended.t, cached.t2d); - - // Z1 * Z2 - cached.multByZ(partialXYZT.xyz.x, extended.xyz.z); - - // D = 2 * Z1 * Z2 - Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x); - - // X3 = B - A - Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Y3 = B + A - Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Z3 = D + C - Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t); - - // T3 = D - C - Field25519.sub(partialXYZT.t, t, partialXYZT.t); - } - - /** - * Based on the addition defined in Section 3.1 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * Please note that this is a partial of the operation listed there leaving out the final - * conversion from PartialXYZT to XYZT. - * - * @param extended extended projective point input - * @param cached cached projective point input - */ - private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) { - long[] t = new long[Field25519.LIMB_CNT]; - - // Y1 + X1 - Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x); - - // Y1 - X1 - Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x); - - // A = (Y1 - X1) * (Y2 + X2) - Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX); - - // B = (Y1 + X1) * (Y2 - X2) - Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX); - - // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper) - Field25519.mult(partialXYZT.t, extended.t, cached.t2d); - - // Z1 * Z2 - cached.multByZ(partialXYZT.xyz.x, extended.xyz.z); - - // D = 2 * Z1 * Z2 - Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x); - - // X3 = B - A - Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Y3 = B + A - Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Z3 = D - C - Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t); - - // T3 = D + C - Field25519.sum(partialXYZT.t, t, partialXYZT.t); - } - - /** - * Doubles {@code p} and puts the result into this PartialXYZT. - *

- * This is based on the addition defined in formula 7 in Section 3.3 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * Please note that this is a partial of the operation listed there leaving out the final - * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in - * the paper, H should be replaced with A+B. - */ - private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) { - long[] t0 = new long[Field25519.LIMB_CNT]; - - // XX = X1^2 - Field25519.square(partialXYZT.xyz.x, p.x); - - // YY = Y1^2 - Field25519.square(partialXYZT.xyz.z, p.y); - - // B' = Z1^2 - Field25519.square(partialXYZT.t, p.z); - - // B = 2 * B' - Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t); - - // A = X1 + Y1 - Field25519.sum(partialXYZT.xyz.y, p.x, p.y); - - // AA = A^2 - Field25519.square(t0, partialXYZT.xyz.y); - - // Y3 = YY + XX - Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x); - - // Z3 = YY - XX - Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x); - - // X3 = AA - Y3 - Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y); - - // T3 = B - Z3 - Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z); - } - - /** - * Doubles {@code p} and puts the result into this PartialXYZT. - */ - private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) { - doubleXYZ(partialXYZT, p.xyz); - } - - /** - * Compares two byte values in constant time. - */ - private static int eq(int a, int b) { - int r = ~(a ^ b) & 0xff; - r &= r << 4; - r &= r << 2; - r &= r << 1; - return (r >> 7) & 1; - } - - /** - * This is a constant time operation where point b*B*256^pos is stored in {@code t}. - * When b is 0, t remains the same (i.e., neutral point). - *

- * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select - * method negates the corresponding point if b is negative (which is straight forward in elliptic - * curves by just negating y coordinate). Therefore we can get multiples of B with the half of - * memory requirements. - * - * @param t neutral element (i.e., point 0), also serves as output. - * @param pos in B[pos][j] = (j+1)*B*256^pos - * @param b value in [-8, 8] range. - */ - private static void select(CachedXYT t, int pos, byte b) { - int bnegative = (b & 0xff) >> 7; - int babs = b - (((-bnegative) & b) << 1); - - t.copyConditional(B_TABLE[pos][0], eq(babs, 1)); - t.copyConditional(B_TABLE[pos][1], eq(babs, 2)); - t.copyConditional(B_TABLE[pos][2], eq(babs, 3)); - t.copyConditional(B_TABLE[pos][3], eq(babs, 4)); - t.copyConditional(B_TABLE[pos][4], eq(babs, 5)); - t.copyConditional(B_TABLE[pos][5], eq(babs, 6)); - t.copyConditional(B_TABLE[pos][6], eq(babs, 7)); - t.copyConditional(B_TABLE[pos][7], eq(babs, 8)); - - long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT); - long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT); - long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT); - neg(t2d, t2d); - CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d); - t.copyConditional(minust, bnegative); - } - - /** - * Computes {@code a}*B - * where a = a[0]+256*a[1]+...+256^31 a[31] and - * B is the Ed25519 base point (x,4/5) with x positive. - *

- * Preconditions: - * a[31] <= 127 - * - * @throws IllegalStateException iff there is arithmetic error. - */ - @SuppressWarnings("NarrowingCompoundAssignment") - private static XYZ scalarMultWithBase(byte[] a) { - byte[] e = new byte[2 * Field25519.FIELD_LEN]; - for (int i = 0; i < Field25519.FIELD_LEN; i++) { - e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf); - e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf); - } - // each e[i] is between 0 and 15 - // e[63] is between 0 and 7 - - // Rewrite e in a way that each e[i] is in [-8, 8]. - // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte - // a[63] can be at most 1. - int carry = 0; - for (int i = 0; i < e.length - 1; i++) { - e[i] += carry; - carry = e[i] + 8; - carry >>= 4; - e[i] -= carry << 4; - } - e[e.length - 1] += carry; - - PartialXYZT ret = new PartialXYZT(NEUTRAL); - XYZT xyzt = new XYZT(); - // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64 - // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd - // indices. After the result, we can double the result point 4 times to shift the multiplication - // scalar by 4 bits. - for (int i = 1; i < e.length; i += 2) { - CachedXYT t = new CachedXYT(CACHED_NEUTRAL); - select(t, i / 2, e[i]); - add(ret, XYZT.fromPartialXYZT(xyzt, ret), t); - } - - // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result - // for the odd indices in e. - XYZ xyz = new XYZ(); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - - // Add multiples of B for even indices of e. - for (int i = 0; i < e.length; i += 2) { - CachedXYT t = new CachedXYT(CACHED_NEUTRAL); - select(t, i / 2, e[i]); - add(ret, XYZT.fromPartialXYZT(xyzt, ret), t); - } - - // This check is to protect against flaws, i.e. if there is a computation error through a - // faulty CPU or if the implementation contains a bug. - XYZ result = new XYZ(ret); - if (!result.isOnCurve()) { - throw new IllegalStateException("arithmetic error in scalar multiplication"); - } - return result; - } - - @SuppressWarnings("NarrowingCompoundAssignment") - private static byte[] slide(byte[] a) { - byte[] r = new byte[256]; - // Writes each bit in a[0..31] into r[0..255]: - // a = a[0]+256*a[1]+...+256^31*a[31] is equal to - // r = r[0]+2*r[1]+...+2^255*r[255] - for (int i = 0; i < 256; i++) { - r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7))); - } - - // Transforms r[i] as odd values in [-15, 15] - for (int i = 0; i < 256; i++) { - if (r[i] != 0) { - for (int b = 1; b <= 6 && i + b < 256; b++) { - if (r[i + b] != 0) { - if (r[i] + (r[i + b] << b) <= 15) { - r[i] += r[i + b] << b; - r[i + b] = 0; - } else if (r[i] - (r[i + b] << b) >= -15) { - r[i] -= r[i + b] << b; - for (int k = i + b; k < 256; k++) { - if (r[k] == 0) { - r[k] = 1; - break; - } - r[k] = 0; - } - } else { - break; - } - } - } - } - } - return r; - } - - /** - * Computes {@code a}*{@code pointA}+{@code b}*B - * where a = a[0]+256*a[1]+...+256^31*a[31]. - * and b = b[0]+256*b[1]+...+256^31*b[31]. - * B is the Ed25519 base point (x,4/5) with x positive. - *

- * Note that execution time varies based on the input since this will only be used in verification - * of signatures. - */ - private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) { - // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA - CachedXYZT[] pointAArray = new CachedXYZT[8]; - pointAArray[0] = new CachedXYZT(pointA); - PartialXYZT t = new PartialXYZT(); - doubleXYZT(t, pointA); - XYZT doubleA = new XYZT(t); - for (int i = 1; i < pointAArray.length; i++) { - add(t, doubleA, pointAArray[i - 1]); - pointAArray[i] = new CachedXYZT(new XYZT(t)); - } - - byte[] aSlide = slide(a); - byte[] bSlide = slide(b); - t = new PartialXYZT(NEUTRAL); - XYZT u = new XYZT(); - int i = 255; - for (; i >= 0; i--) { - if (aSlide[i] != 0 || bSlide[i] != 0) { - break; - } - } - for (; i >= 0; i--) { - doubleXYZ(t, new XYZ(t)); - if (aSlide[i] > 0) { - add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]); - } else if (aSlide[i] < 0) { - sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]); - } - if (bSlide[i] > 0) { - add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]); - } else if (bSlide[i] < 0) { - sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]); - } - } - - return new XYZ(t); - } - - /** - * Returns true if {@code in} is nonzero. - *

- * Note that execution time might depend on the input {@code in}. - */ - private static boolean isNonZeroVarTime(long[] in) { - long[] inCopy = new long[in.length + 1]; - System.arraycopy(in, 0, inCopy, 0, in.length); - Field25519.reduceCoefficients(inCopy); - byte[] bytes = Field25519.contract(inCopy); - for (byte b : bytes) { - if (b != 0) { - return true; - } - } - return false; - } - - /** - * Returns the least significant bit of {@code in}. - */ - private static int getLsb(long[] in) { - return Field25519.contract(in)[0] & 1; - } - - /** - * Negates all values in {@code in} and store it in {@code out}. - */ - private static void neg(long[] out, long[] in) { - for (int i = 0; i < in.length; i++) { - out[i] = -in[i]; - } - } - - /** - * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}. - */ - private static void pow2252m3(long[] out, long[] in) { - long[] t0 = new long[Field25519.LIMB_CNT]; - long[] t1 = new long[Field25519.LIMB_CNT]; - long[] t2 = new long[Field25519.LIMB_CNT]; - - // z2 = z1^2^1 - Field25519.square(t0, in); - - // z8 = z2^2^2 - Field25519.square(t1, t0); - for (int i = 1; i < 2; i++) { - Field25519.square(t1, t1); - } - - // z9 = z1*z8 - Field25519.mult(t1, in, t1); - - // z11 = z2*z9 - Field25519.mult(t0, t0, t1); - - // z22 = z11^2^1 - Field25519.square(t0, t0); - - // z_5_0 = z9*z22 - Field25519.mult(t0, t1, t0); - - // z_10_5 = z_5_0^2^5 - Field25519.square(t1, t0); - for (int i = 1; i < 5; i++) { - Field25519.square(t1, t1); - } - - // z_10_0 = z_10_5*z_5_0 - Field25519.mult(t0, t1, t0); - - // z_20_10 = z_10_0^2^10 - Field25519.square(t1, t0); - for (int i = 1; i < 10; i++) { - Field25519.square(t1, t1); - } - - // z_20_0 = z_20_10*z_10_0 - Field25519.mult(t1, t1, t0); - - // z_40_20 = z_20_0^2^20 - Field25519.square(t2, t1); - for (int i = 1; i < 20; i++) { - Field25519.square(t2, t2); - } - - // z_40_0 = z_40_20*z_20_0 - Field25519.mult(t1, t2, t1); - - // z_50_10 = z_40_0^2^10 - Field25519.square(t1, t1); - for (int i = 1; i < 10; i++) { - Field25519.square(t1, t1); - } - - // z_50_0 = z_50_10*z_10_0 - Field25519.mult(t0, t1, t0); - - // z_100_50 = z_50_0^2^50 - Field25519.square(t1, t0); - for (int i = 1; i < 50; i++) { - Field25519.square(t1, t1); - } - - // z_100_0 = z_100_50*z_50_0 - Field25519.mult(t1, t1, t0); - - // z_200_100 = z_100_0^2^100 - Field25519.square(t2, t1); - for (int i = 1; i < 100; i++) { - Field25519.square(t2, t2); - } - - // z_200_0 = z_200_100*z_100_0 - Field25519.mult(t1, t2, t1); - - // z_250_50 = z_200_0^2^50 - Field25519.square(t1, t1); - for (int i = 1; i < 50; i++) { - Field25519.square(t1, t1); - } - - // z_250_0 = z_250_50*z_50_0 - Field25519.mult(t0, t1, t0); - - // z_252_2 = z_250_0^2^2 - Field25519.square(t0, t0); - for (int i = 1; i < 2; i++) { - Field25519.square(t0, t0); - } - - // z_252_3 = z_252_2*z1 - Field25519.mult(out, t0, in); - } - - /** - * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format. - */ - private static long load3(byte[] in, int idx) { - long result; - result = (long) in[idx] & 0xff; - result |= (long) (in[idx + 1] & 0xff) << 8; - result |= (long) (in[idx + 2] & 0xff) << 16; - return result; - } - - /** - * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format. - */ - private static long load4(byte[] in, int idx) { - long result = load3(in, idx); - result |= (long) (in[idx + 3] & 0xff) << 24; - return result; - } - - /** - * Input: - * s[0]+256*s[1]+...+256^63*s[63] = s - *

- * Output: - * s[0]+256*s[1]+...+256^31*s[31] = s mod l - * where l = 2^252 + 27742317777372353535851937790883648493. - * Overwrites s in place. - */ - private static void reduce(byte[] s) { - // Observation: - // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l - // Let m = -27742317777372353535851937790883648493 - // Thus a*2^252+b mod l is equivalent to a*m+b mod l - // - // First s is divided into chunks of 21 bits as follows: - // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63] - long s0 = 2097151 & load3(s, 0); - long s1 = 2097151 & (load4(s, 2) >> 5); - long s2 = 2097151 & (load3(s, 5) >> 2); - long s3 = 2097151 & (load4(s, 7) >> 7); - long s4 = 2097151 & (load4(s, 10) >> 4); - long s5 = 2097151 & (load3(s, 13) >> 1); - long s6 = 2097151 & (load4(s, 15) >> 6); - long s7 = 2097151 & (load3(s, 18) >> 3); - long s8 = 2097151 & load3(s, 21); - long s9 = 2097151 & (load4(s, 23) >> 5); - long s10 = 2097151 & (load3(s, 26) >> 2); - long s11 = 2097151 & (load4(s, 28) >> 7); - long s12 = 2097151 & (load4(s, 31) >> 4); - long s13 = 2097151 & (load3(s, 34) >> 1); - long s14 = 2097151 & (load4(s, 36) >> 6); - long s15 = 2097151 & (load3(s, 39) >> 3); - long s16 = 2097151 & load3(s, 42); - long s17 = 2097151 & (load4(s, 44) >> 5); - long s18 = 2097151 & (load3(s, 47) >> 2); - long s19 = 2097151 & (load4(s, 49) >> 7); - long s20 = 2097151 & (load4(s, 52) >> 4); - long s21 = 2097151 & (load3(s, 55) >> 1); - long s22 = 2097151 & (load4(s, 57) >> 6); - long s23 = (load4(s, 60) >> 3); - long carry0; - long carry1; - long carry2; - long carry3; - long carry4; - long carry5; - long carry6; - long carry7; - long carry8; - long carry9; - long carry10; - long carry11; - long carry12; - long carry13; - long carry14; - long carry15; - long carry16; - - // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l - // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6) - // starting from s11 (s11*2^210) - // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs - s11 += s23 * 666643; - s12 += s23 * 470296; - s13 += s23 * 654183; - s14 -= s23 * 997805; - s15 += s23 * 136657; - s16 -= s23 * 683901; - // s23 = 0; - - s10 += s22 * 666643; - s11 += s22 * 470296; - s12 += s22 * 654183; - s13 -= s22 * 997805; - s14 += s22 * 136657; - s15 -= s22 * 683901; - // s22 = 0; - - s9 += s21 * 666643; - s10 += s21 * 470296; - s11 += s21 * 654183; - s12 -= s21 * 997805; - s13 += s21 * 136657; - s14 -= s21 * 683901; - // s21 = 0; - - s8 += s20 * 666643; - s9 += s20 * 470296; - s10 += s20 * 654183; - s11 -= s20 * 997805; - s12 += s20 * 136657; - s13 -= s20 * 683901; - // s20 = 0; - - s7 += s19 * 666643; - s8 += s19 * 470296; - s9 += s19 * 654183; - s10 -= s19 * 997805; - s11 += s19 * 136657; - s12 -= s19 * 683901; - // s19 = 0; - - s6 += s18 * 666643; - s7 += s18 * 470296; - s8 += s18 * 654183; - s9 -= s18 * 997805; - s10 += s18 * 136657; - s11 -= s18 * 683901; - // s18 = 0; - - // Reduce the bit length of limbs from s6 to s15 to 21-bits. - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry12 = (s12 + (1 << 20)) >> 21; - s13 += carry12; - s12 -= carry12 << 21; - carry14 = (s14 + (1 << 20)) >> 21; - s15 += carry14; - s14 -= carry14 << 21; - carry16 = (s16 + (1 << 20)) >> 21; - s17 += carry16; - s16 -= carry16 << 21; - - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - carry13 = (s13 + (1 << 20)) >> 21; - s14 += carry13; - s13 -= carry13 << 21; - carry15 = (s15 + (1 << 20)) >> 21; - s16 += carry15; - s15 -= carry15 << 21; - - // Resume reduction where we left off. - s5 += s17 * 666643; - s6 += s17 * 470296; - s7 += s17 * 654183; - s8 -= s17 * 997805; - s9 += s17 * 136657; - s10 -= s17 * 683901; - // s17 = 0; - - s4 += s16 * 666643; - s5 += s16 * 470296; - s6 += s16 * 654183; - s7 -= s16 * 997805; - s8 += s16 * 136657; - s9 -= s16 * 683901; - // s16 = 0; - - s3 += s15 * 666643; - s4 += s15 * 470296; - s5 += s15 * 654183; - s6 -= s15 * 997805; - s7 += s15 * 136657; - s8 -= s15 * 683901; - // s15 = 0; - - s2 += s14 * 666643; - s3 += s14 * 470296; - s4 += s14 * 654183; - s5 -= s14 * 997805; - s6 += s14 * 136657; - s7 -= s14 * 683901; - // s14 = 0; - - s1 += s13 * 666643; - s2 += s13 * 470296; - s3 += s13 * 654183; - s4 -= s13 * 997805; - s5 += s13 * 136657; - s6 -= s13 * 683901; - // s13 = 0; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - // Reduce the range of limbs from s0 to s11 to 21-bits. - carry0 = (s0 + (1 << 20)) >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry2 = (s2 + (1 << 20)) >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry4 = (s4 + (1 << 20)) >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - carry1 = (s1 + (1 << 20)) >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry3 = (s3 + (1 << 20)) >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry5 = (s5 + (1 << 20)) >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs. - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry11 = s11 >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - // Do one last reduction as s12 might be 1. - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - // s12 = 0; - - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - // Serialize the result into the s. - s[0] = (byte) s0; - s[1] = (byte) (s0 >> 8); - s[2] = (byte) ((s0 >> 16) | (s1 << 5)); - s[3] = (byte) (s1 >> 3); - s[4] = (byte) (s1 >> 11); - s[5] = (byte) ((s1 >> 19) | (s2 << 2)); - s[6] = (byte) (s2 >> 6); - s[7] = (byte) ((s2 >> 14) | (s3 << 7)); - s[8] = (byte) (s3 >> 1); - s[9] = (byte) (s3 >> 9); - s[10] = (byte) ((s3 >> 17) | (s4 << 4)); - s[11] = (byte) (s4 >> 4); - s[12] = (byte) (s4 >> 12); - s[13] = (byte) ((s4 >> 20) | (s5 << 1)); - s[14] = (byte) (s5 >> 7); - s[15] = (byte) ((s5 >> 15) | (s6 << 6)); - s[16] = (byte) (s6 >> 2); - s[17] = (byte) (s6 >> 10); - s[18] = (byte) ((s6 >> 18) | (s7 << 3)); - s[19] = (byte) (s7 >> 5); - s[20] = (byte) (s7 >> 13); - s[21] = (byte) s8; - s[22] = (byte) (s8 >> 8); - s[23] = (byte) ((s8 >> 16) | (s9 << 5)); - s[24] = (byte) (s9 >> 3); - s[25] = (byte) (s9 >> 11); - s[26] = (byte) ((s9 >> 19) | (s10 << 2)); - s[27] = (byte) (s10 >> 6); - s[28] = (byte) ((s10 >> 14) | (s11 << 7)); - s[29] = (byte) (s11 >> 1); - s[30] = (byte) (s11 >> 9); - s[31] = (byte) (s11 >> 17); - } - - /** - * Input: - * a[0]+256*a[1]+...+256^31*a[31] = a - * b[0]+256*b[1]+...+256^31*b[31] = b - * c[0]+256*c[1]+...+256^31*c[31] = c - *

- * Output: - * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l - * where l = 2^252 + 27742317777372353535851937790883648493. - */ - private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) { - // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c - // See Ed25519.reduce for related comments. - long a0 = 2097151 & load3(a, 0); - long a1 = 2097151 & (load4(a, 2) >> 5); - long a2 = 2097151 & (load3(a, 5) >> 2); - long a3 = 2097151 & (load4(a, 7) >> 7); - long a4 = 2097151 & (load4(a, 10) >> 4); - long a5 = 2097151 & (load3(a, 13) >> 1); - long a6 = 2097151 & (load4(a, 15) >> 6); - long a7 = 2097151 & (load3(a, 18) >> 3); - long a8 = 2097151 & load3(a, 21); - long a9 = 2097151 & (load4(a, 23) >> 5); - long a10 = 2097151 & (load3(a, 26) >> 2); - long a11 = (load4(a, 28) >> 7); - long b0 = 2097151 & load3(b, 0); - long b1 = 2097151 & (load4(b, 2) >> 5); - long b2 = 2097151 & (load3(b, 5) >> 2); - long b3 = 2097151 & (load4(b, 7) >> 7); - long b4 = 2097151 & (load4(b, 10) >> 4); - long b5 = 2097151 & (load3(b, 13) >> 1); - long b6 = 2097151 & (load4(b, 15) >> 6); - long b7 = 2097151 & (load3(b, 18) >> 3); - long b8 = 2097151 & load3(b, 21); - long b9 = 2097151 & (load4(b, 23) >> 5); - long b10 = 2097151 & (load3(b, 26) >> 2); - long b11 = (load4(b, 28) >> 7); - long c0 = 2097151 & load3(c, 0); - long c1 = 2097151 & (load4(c, 2) >> 5); - long c2 = 2097151 & (load3(c, 5) >> 2); - long c3 = 2097151 & (load4(c, 7) >> 7); - long c4 = 2097151 & (load4(c, 10) >> 4); - long c5 = 2097151 & (load3(c, 13) >> 1); - long c6 = 2097151 & (load4(c, 15) >> 6); - long c7 = 2097151 & (load3(c, 18) >> 3); - long c8 = 2097151 & load3(c, 21); - long c9 = 2097151 & (load4(c, 23) >> 5); - long c10 = 2097151 & (load3(c, 26) >> 2); - long c11 = (load4(c, 28) >> 7); - long s0; - long s1; - long s2; - long s3; - long s4; - long s5; - long s6; - long s7; - long s8; - long s9; - long s10; - long s11; - long s12; - long s13; - long s14; - long s15; - long s16; - long s17; - long s18; - long s19; - long s20; - long s21; - long s22; - long s23; - long carry0; - long carry1; - long carry2; - long carry3; - long carry4; - long carry5; - long carry6; - long carry7; - long carry8; - long carry9; - long carry10; - long carry11; - long carry12; - long carry13; - long carry14; - long carry15; - long carry16; - long carry17; - long carry18; - long carry19; - long carry20; - long carry21; - long carry22; - - s0 = c0 + a0 * b0; - s1 = c1 + a0 * b1 + a1 * b0; - s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; - s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; - s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; - s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; - s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; - s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; - s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 - + a8 * b0; - s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 - + a8 * b1 + a9 * b0; - s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 - + a8 * b2 + a9 * b1 + a10 * b0; - s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 - + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; - s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 - + a10 * b2 + a11 * b1; - s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 - + a11 * b2; - s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 - + a11 * b3; - s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; - s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; - s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; - s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; - s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; - s20 = a9 * b11 + a10 * b10 + a11 * b9; - s21 = a10 * b11 + a11 * b10; - s22 = a11 * b11; - s23 = 0; - - carry0 = (s0 + (1 << 20)) >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry2 = (s2 + (1 << 20)) >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry4 = (s4 + (1 << 20)) >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry12 = (s12 + (1 << 20)) >> 21; - s13 += carry12; - s12 -= carry12 << 21; - carry14 = (s14 + (1 << 20)) >> 21; - s15 += carry14; - s14 -= carry14 << 21; - carry16 = (s16 + (1 << 20)) >> 21; - s17 += carry16; - s16 -= carry16 << 21; - carry18 = (s18 + (1 << 20)) >> 21; - s19 += carry18; - s18 -= carry18 << 21; - carry20 = (s20 + (1 << 20)) >> 21; - s21 += carry20; - s20 -= carry20 << 21; - carry22 = (s22 + (1 << 20)) >> 21; - s23 += carry22; - s22 -= carry22 << 21; - - carry1 = (s1 + (1 << 20)) >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry3 = (s3 + (1 << 20)) >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry5 = (s5 + (1 << 20)) >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - carry13 = (s13 + (1 << 20)) >> 21; - s14 += carry13; - s13 -= carry13 << 21; - carry15 = (s15 + (1 << 20)) >> 21; - s16 += carry15; - s15 -= carry15 << 21; - carry17 = (s17 + (1 << 20)) >> 21; - s18 += carry17; - s17 -= carry17 << 21; - carry19 = (s19 + (1 << 20)) >> 21; - s20 += carry19; - s19 -= carry19 << 21; - carry21 = (s21 + (1 << 20)) >> 21; - s22 += carry21; - s21 -= carry21 << 21; - - s11 += s23 * 666643; - s12 += s23 * 470296; - s13 += s23 * 654183; - s14 -= s23 * 997805; - s15 += s23 * 136657; - s16 -= s23 * 683901; - // s23 = 0; - - s10 += s22 * 666643; - s11 += s22 * 470296; - s12 += s22 * 654183; - s13 -= s22 * 997805; - s14 += s22 * 136657; - s15 -= s22 * 683901; - // s22 = 0; - - s9 += s21 * 666643; - s10 += s21 * 470296; - s11 += s21 * 654183; - s12 -= s21 * 997805; - s13 += s21 * 136657; - s14 -= s21 * 683901; - // s21 = 0; - - s8 += s20 * 666643; - s9 += s20 * 470296; - s10 += s20 * 654183; - s11 -= s20 * 997805; - s12 += s20 * 136657; - s13 -= s20 * 683901; - // s20 = 0; - - s7 += s19 * 666643; - s8 += s19 * 470296; - s9 += s19 * 654183; - s10 -= s19 * 997805; - s11 += s19 * 136657; - s12 -= s19 * 683901; - // s19 = 0; - - s6 += s18 * 666643; - s7 += s18 * 470296; - s8 += s18 * 654183; - s9 -= s18 * 997805; - s10 += s18 * 136657; - s11 -= s18 * 683901; - // s18 = 0; - - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry12 = (s12 + (1 << 20)) >> 21; - s13 += carry12; - s12 -= carry12 << 21; - carry14 = (s14 + (1 << 20)) >> 21; - s15 += carry14; - s14 -= carry14 << 21; - carry16 = (s16 + (1 << 20)) >> 21; - s17 += carry16; - s16 -= carry16 << 21; - - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - carry13 = (s13 + (1 << 20)) >> 21; - s14 += carry13; - s13 -= carry13 << 21; - carry15 = (s15 + (1 << 20)) >> 21; - s16 += carry15; - s15 -= carry15 << 21; - - s5 += s17 * 666643; - s6 += s17 * 470296; - s7 += s17 * 654183; - s8 -= s17 * 997805; - s9 += s17 * 136657; - s10 -= s17 * 683901; - // s17 = 0; - - s4 += s16 * 666643; - s5 += s16 * 470296; - s6 += s16 * 654183; - s7 -= s16 * 997805; - s8 += s16 * 136657; - s9 -= s16 * 683901; - // s16 = 0; - - s3 += s15 * 666643; - s4 += s15 * 470296; - s5 += s15 * 654183; - s6 -= s15 * 997805; - s7 += s15 * 136657; - s8 -= s15 * 683901; - // s15 = 0; - - s2 += s14 * 666643; - s3 += s14 * 470296; - s4 += s14 * 654183; - s5 -= s14 * 997805; - s6 += s14 * 136657; - s7 -= s14 * 683901; - // s14 = 0; - - s1 += s13 * 666643; - s2 += s13 * 470296; - s3 += s13 * 654183; - s4 -= s13 * 997805; - s5 += s13 * 136657; - s6 -= s13 * 683901; - // s13 = 0; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry0 = (s0 + (1 << 20)) >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry2 = (s2 + (1 << 20)) >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry4 = (s4 + (1 << 20)) >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - carry1 = (s1 + (1 << 20)) >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry3 = (s3 + (1 << 20)) >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry5 = (s5 + (1 << 20)) >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry11 = s11 >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - // s12 = 0; - - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - s[0] = (byte) s0; - s[1] = (byte) (s0 >> 8); - s[2] = (byte) ((s0 >> 16) | (s1 << 5)); - s[3] = (byte) (s1 >> 3); - s[4] = (byte) (s1 >> 11); - s[5] = (byte) ((s1 >> 19) | (s2 << 2)); - s[6] = (byte) (s2 >> 6); - s[7] = (byte) ((s2 >> 14) | (s3 << 7)); - s[8] = (byte) (s3 >> 1); - s[9] = (byte) (s3 >> 9); - s[10] = (byte) ((s3 >> 17) | (s4 << 4)); - s[11] = (byte) (s4 >> 4); - s[12] = (byte) (s4 >> 12); - s[13] = (byte) ((s4 >> 20) | (s5 << 1)); - s[14] = (byte) (s5 >> 7); - s[15] = (byte) ((s5 >> 15) | (s6 << 6)); - s[16] = (byte) (s6 >> 2); - s[17] = (byte) (s6 >> 10); - s[18] = (byte) ((s6 >> 18) | (s7 << 3)); - s[19] = (byte) (s7 >> 5); - s[20] = (byte) (s7 >> 13); - s[21] = (byte) s8; - s[22] = (byte) (s8 >> 8); - s[23] = (byte) ((s8 >> 16) | (s9 << 5)); - s[24] = (byte) (s9 >> 3); - s[25] = (byte) (s9 >> 11); - s[26] = (byte) ((s9 >> 19) | (s10 << 2)); - s[27] = (byte) (s10 >> 6); - s[28] = (byte) ((s10 >> 14) | (s11 << 7)); - s[29] = (byte) (s11 >> 1); - s[30] = (byte) (s11 >> 9); - s[31] = (byte) (s11 >> 17); - } - - // The order of the generator as unsigned bytes in little endian order. - // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748) - private static final byte[] GROUP_ORDER = { - (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, - (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58, - (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, - (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10}; - - // Checks whether s represents an integer smaller than the order of the group. - // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check - // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.) - // @param s an integer in little-endian order. - private static boolean isSmallerThanGroupOrder(byte[] s) { - for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) { - // compare unsigned bytes - int a = s[j] & 0xff; - int b = GROUP_ORDER[j] & 0xff; - if (a != b) { - return a < b; - } - } - return false; - } - - /** - * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with - * {@code publicKey}. - */ - public static boolean verify(final byte[] message, final byte[] signature, - final byte[] publicKey) { - try { - if (signature.length != SIGNATURE_LEN) { - return false; - } - if (publicKey.length != PUBLIC_KEY_LEN) { - return false; - } - byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN); - if (!isSmallerThanGroupOrder(s)) { - return false; - } - MessageDigest digest = MessageDigest.getInstance("SHA-512"); - digest.update(signature, 0, Field25519.FIELD_LEN); - digest.update(publicKey); - digest.update(message); - byte[] h = digest.digest(); - reduce(h); - - XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey); - XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s); - byte[] expectedR = xyz.toBytes(); - for (int i = 0; i < Field25519.FIELD_LEN; i++) { - if (expectedR[i] != signature[i]) { - return false; - } - } - return true; - } catch (final GeneralSecurityException ignored) { - return false; - } - } -} diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java deleted file mode 100644 index 9e25e6057..000000000 --- a/app/src/main/java/com/wireguard/crypto/Key.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import com.wireguard.crypto.KeyFormatException.Type; - -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Arrays; - -/** - * Represents a WireGuard public or private key. This class uses specialized constant-time base64 - * and hexadecimal codec implementations that resist side-channel attacks. - *

- * Instances of this class are immutable. - */ -@SuppressWarnings("MagicNumber") -public final class Key { - private final byte[] key; - - /** - * Constructs an object encapsulating the supplied key. - * - * @param key an array of bytes containing a binary key. Callers of this constructor are - * responsible for ensuring that the array is of the correct length. - */ - private Key(final byte[] key) { - // Defensively copy to ensure immutability. - this.key = Arrays.copyOf(key, key.length); - } - - /** - * Decodes a single 4-character base64 chunk to an integer in constant time. - * - * @param src an array of at least 4 characters in base64 format - * @param srcOffset the offset of the beginning of the chunk in {@code src} - * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not - * valid base64 - */ - private static int decodeBase64(final char[] src, final int srcOffset) { - int val = 0; - for (int i = 0; i < 4; ++i) { - final char c = src[i + srcOffset]; - val |= (-1 - + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64)) - + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70)) - + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5)) - + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63) - + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64) - ) << (18 - 6 * i); - } - return val; - } - - /** - * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time. - * - * @param src an array of at least 3 bytes - * @param srcOffset the offset of the beginning of the chunk in {@code src} - * @param dest an array of at least 4 characters - * @param destOffset the offset of the beginning of the chunk in {@code dest} - */ - private static void encodeBase64(final byte[] src, final int srcOffset, - final char[] dest, final int destOffset) { - final byte[] input = { - (byte) ((src[srcOffset] >>> 2) & 63), - (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63), - (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63), - (byte) ((src[2 + srcOffset]) & 63), - }; - for (int i = 0; i < 4; ++i) { - dest[i + destOffset] = (char) (input[i] + 'A' - + (((25 - input[i]) >>> 8) & 6) - - (((51 - input[i]) >>> 8) & 75) - - (((61 - input[i]) >>> 8) & 15) - + (((62 - input[i]) >>> 8) & 3)); - } - } - - /** - * Decodes a WireGuard public or private key from its base64 string representation. This - * function throws a {@link KeyFormatException} if the source string is not well-formed. - * - * @param str the base64 string representation of a WireGuard key - * @return the decoded key encapsulated in an immutable container - */ - public static Key fromBase64(final String str) throws KeyFormatException { - final char[] input = str.toCharArray(); - if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') - throw new KeyFormatException(Format.BASE64, Type.LENGTH); - final byte[] key = new byte[Format.BINARY.length]; - int i; - int ret = 0; - for (i = 0; i < key.length / 3; ++i) { - final int val = decodeBase64(input, i * 4); - ret |= val >>> 31; - key[i * 3] = (byte) ((val >>> 16) & 0xff); - key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); - key[i * 3 + 2] = (byte) (val & 0xff); - } - final char[] endSegment = { - input[i * 4], - input[i * 4 + 1], - input[i * 4 + 2], - 'A', - }; - final int val = decodeBase64(endSegment, 0); - ret |= (val >>> 31) | (val & 0xff); - key[i * 3] = (byte) ((val >>> 16) & 0xff); - key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); - - if (ret != 0) - throw new KeyFormatException(Format.BASE64, Type.CONTENTS); - return new Key(key); - } - - /** - * Wraps a WireGuard public or private key in an immutable container. This function throws a - * {@link KeyFormatException} if the source data is not the correct length. - * - * @param bytes an array of bytes containing a WireGuard key in binary format - * @return the key encapsulated in an immutable container - */ - public static Key fromBytes(final byte[] bytes) throws KeyFormatException { - if (bytes.length != Format.BINARY.length) - throw new KeyFormatException(Format.BINARY, Type.LENGTH); - return new Key(bytes); - } - - /** - * Decodes a WireGuard public or private key from its hexadecimal string representation. This - * function throws a {@link KeyFormatException} if the source string is not well-formed. - * - * @param str the hexadecimal string representation of a WireGuard key - * @return the decoded key encapsulated in an immutable container - */ - public static Key fromHex(final String str) throws KeyFormatException { - final char[] input = str.toCharArray(); - if (input.length != Format.HEX.length) - throw new KeyFormatException(Format.HEX, Type.LENGTH); - final byte[] key = new byte[Format.BINARY.length]; - int ret = 0; - for (int i = 0; i < key.length; ++i) { - int c; - int cNum; - int cNum0; - int cAlpha; - int cAlpha0; - int cVal; - final int cAcc; - - c = input[i * 2]; - cNum = c ^ 48; - cNum0 = ((cNum - 10) >>> 8) & 0xff; - cAlpha = (c & ~32) - 55; - cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; - ret |= ((cNum0 | cAlpha0) - 1) >>> 8; - cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); - cAcc = cVal * 16; - - c = input[i * 2 + 1]; - cNum = c ^ 48; - cNum0 = ((cNum - 10) >>> 8) & 0xff; - cAlpha = (c & ~32) - 55; - cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; - ret |= ((cNum0 | cAlpha0) - 1) >>> 8; - cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); - key[i] = (byte) (cAcc | cVal); - } - if (ret != 0) - throw new KeyFormatException(Format.HEX, Type.CONTENTS); - return new Key(key); - } - - /** - * Generates a private key using the system's {@link SecureRandom} number generator. - * - * @return a well-formed random private key - */ - static Key generatePrivateKey() { - final SecureRandom secureRandom = new SecureRandom(); - final byte[] privateKey = new byte[Format.BINARY.getLength()]; - secureRandom.nextBytes(privateKey); - privateKey[0] &= 248; - privateKey[31] &= 127; - privateKey[31] |= 64; - return new Key(privateKey); - } - - /** - * Generates a public key from an existing private key. - * - * @param privateKey a private key - * @return a well-formed public key that corresponds to the supplied private key - */ - static Key generatePublicKey(final Key privateKey) { - final byte[] publicKey = new byte[Format.BINARY.getLength()]; - Curve25519.eval(publicKey, 0, privateKey.getBytes(), null); - return new Key(publicKey); - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) - return true; - if (obj == null || obj.getClass() != getClass()) - return false; - final Key other = (Key) obj; - return MessageDigest.isEqual(key, other.key); - } - - /** - * Returns the key as an array of bytes. - * - * @return an array of bytes containing the raw binary key - */ - public byte[] getBytes() { - // Defensively copy to ensure immutability. - return Arrays.copyOf(key, key.length); - } - - @Override - public int hashCode() { - int ret = 0; - for (int i = 0; i < key.length / 4; ++i) - ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24); - return ret; - } - - /** - * Encodes the key to base64. - * - * @return a string containing the encoded key - */ - public String toBase64() { - final char[] output = new char[Format.BASE64.length]; - int i; - for (i = 0; i < key.length / 3; ++i) - encodeBase64(key, i * 3, output, i * 4); - final byte[] endSegment = { - key[i * 3], - key[i * 3 + 1], - 0, - }; - encodeBase64(endSegment, 0, output, i * 4); - output[Format.BASE64.length - 1] = '='; - return new String(output); - } - - /** - * Encodes the key to hexadecimal ASCII characters. - * - * @return a string containing the encoded key - */ - public String toHex() { - final char[] output = new char[Format.HEX.length]; - for (int i = 0; i < key.length; ++i) { - output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf) - + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38)); - output[i * 2 + 1] = (char) (87 + (key[i] & 0xf) - + ((((key[i] & 0xf) - 10) >> 8) & ~38)); - } - return new String(output); - } - - /** - * The supported formats for encoding a WireGuard key. - */ - public enum Format { - BASE64(44), - BINARY(32), - HEX(64); - - private final int length; - - Format(final int length) { - this.length = length; - } - - public int getLength() { - return length; - } - } - -} diff --git a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java b/app/src/main/java/com/wireguard/crypto/KeyFormatException.java deleted file mode 100644 index 5818b4d45..000000000 --- a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -/** - * An exception thrown when attempting to parse an invalid key (too short, too long, or byte - * data inappropriate for the format). The format being parsed can be accessed with the - * {@link #getFormat} method. - */ -public final class KeyFormatException extends Exception { - private final Key.Format format; - private final Type type; - - KeyFormatException(final Key.Format format, final Type type) { - this.format = format; - this.type = type; - } - - public Key.Format getFormat() { - return format; - } - - public Type getType() { - return type; - } - - public enum Type { - CONTENTS, - LENGTH - } -} diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/app/src/main/java/com/wireguard/crypto/KeyPair.java deleted file mode 100644 index f8238e91c..000000000 --- a/app/src/main/java/com/wireguard/crypto/KeyPair.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -/** - * Represents a Curve25519 key pair as used by WireGuard. - *

- * Instances of this class are immutable. - */ -public class KeyPair { - private final Key privateKey; - private final Key publicKey; - - /** - * Creates a key pair using a newly-generated private key. - */ - public KeyPair() { - this(Key.generatePrivateKey()); - } - - /** - * Creates a key pair using an existing private key. - * - * @param privateKey a private key, used to derive the public key - */ - public KeyPair(final Key privateKey) { - this.privateKey = privateKey; - publicKey = Key.generatePublicKey(privateKey); - } - - /** - * Returns the private key from the key pair. - * - * @return the private key - */ - public Key getPrivateKey() { - return privateKey; - } - - /** - * Returns the public key from the key pair. - * - * @return the public key - */ - public Key getPublicKey() { - return publicKey; - } -} diff --git a/app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt b/app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt new file mode 100644 index 000000000..fd037ad49 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt @@ -0,0 +1,42 @@ +package io.nekohasekai.sagernet + +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import io.nekohasekai.sagernet.bg.SubscriptionUpdater +import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.ktx.app +import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher + +class BootReceiver : BroadcastReceiver() { + companion object { + private val componentName by lazy { ComponentName(app, BootReceiver::class.java) } + var enabled: Boolean + get() = app.packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + set(value) = app.packageManager.setComponentEnabledSetting( + 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) { + runOnDefaultDispatcher { + SubscriptionUpdater.reconfigureUpdater() + } + + if (!DataStore.persistAcrossReboot) { // sanity check + enabled = false + return + } + + val doStart = when (intent.action) { + Intent.ACTION_LOCKED_BOOT_COMPLETED -> false // DataStore.directBootAware + else -> Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked + } && DataStore.selectedProxy > 0 + + if (doStart) SagerNet.startService() + } +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index d84be134a..caf363f6f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -7,6 +7,8 @@ object Key { const val DB_PUBLIC = "configuration.db" const val DB_PROFILE = "sager_net.db" + const val PERSIST_ACROSS_REBOOT = "isAutoConnect" + const val APP_EXPERT = "isExpert" const val APP_THEME = "appTheme" const val NIGHT_THEME = "nightTheme" @@ -14,12 +16,12 @@ object Key { const val MODE_VPN = "vpn" const val MODE_PROXY = "proxy" + const val GLOBAL_CUSTOM_CONFIG = "globalCustomConfig" + const val REMOTE_DNS = "remoteDns" const val DIRECT_DNS = "directDns" - const val DIRECT_DNS_USE_SYSTEM = "directDnsUseSystem" const val ENABLE_DNS_ROUTING = "enableDnsRouting" const val ENABLE_FAKEDNS = "enableFakeDns" - const val DNS_NETWORK = "dnsNetwork" const val IPV6_MODE = "ipv6Mode" @@ -28,7 +30,6 @@ object Key { const val INDIVIDUAL = "individual" const val METERED_NETWORK = "meteredNetwork" - const val DOMAIN_STRATEGY = "domainStrategy" const val TRAFFIC_SNIFFING = "trafficSniffing" const val RESOLVE_DESTINATION = "resolveDestination" @@ -39,16 +40,13 @@ object Key { const val ALLOW_ACCESS = "allowAccess" const val SPEED_INTERVAL = "speedInterval" const val SHOW_DIRECT_SPEED = "showDirectSpeed" - const val LOCAL_DNS_PORT = "portLocalDns" const val APPEND_HTTP_PROXY = "appendHttpProxy" - const val REQUIRE_TRANSPROXY = "requireTransproxy" - const val TRANSPROXY_MODE = "transproxyMode" - const val TRANSPROXY_PORT = "transproxyPort" const val CONNECTION_TEST_URL = "connectionTestURL" - const val TCP_KEEP_ALIVE_INTERVAL = "tcpKeepAliveInterval" + const val NETWORK_CHANGE_RESET_CONNECTIONS = "networkChangeResetConnections" + const val WAKE_RESET_CONNECTIONS = "wakeResetConnections" const val RULES_PROVIDER = "rulesProvider" const val LOG_LEVEL = "logLevel" const val LOG_BUF_SIZE = "logBufSize" @@ -56,12 +54,13 @@ object Key { const val ALWAYS_SHOW_ADDRESS = "alwaysShowAddress" // Protocol Settings - const val MUX_PROTOCOLS = "mux" - const val MUX_CONCURRENCY = "muxConcurrency" + const val GLOBAL_ALLOW_INSECURE = "globalAllowInsecure" const val ACQUIRE_WAKE_LOCK = "acquireWakeLock" const val SHOW_BOTTOM_BAR = "showBottomBar" + const val ALLOW_INSECURE_ON_REQUEST = "allowInsecureOnRequest" + const val TUN_IMPLEMENTATION = "tunImplementation" const val PROFILE_TRAFFIC_STATISTICS = "profileTrafficStatistics" @@ -76,14 +75,13 @@ object Key { const val SERVER_USERNAME = "serverUsername" const val SERVER_PASSWORD = "serverPassword" const val SERVER_METHOD = "serverMethod" - const val SERVER_PLUGIN = "serverPlugin" - const val SERVER_PLUGIN_CONFIGURE = "serverPluginConfigure" const val SERVER_PASSWORD1 = "serverPassword1" + const val PROTOCOL_VERSION = "protocolVersion" + const val SERVER_PROTOCOL = "serverProtocol" const val SERVER_OBFS = "serverObfs" - const val SERVER_SECURITY = "serverSecurity" const val SERVER_NETWORK = "serverNetwork" const val SERVER_HOST = "serverHost" const val SERVER_PATH = "serverPath" @@ -91,6 +89,7 @@ object Key { const val SERVER_ENCRYPTION = "serverEncryption" const val SERVER_ALPN = "serverALPN" const val SERVER_CERTIFICATES = "serverCertificates" + const val SERVER_MTU = "serverMTU" const val SERVER_CONFIG = "serverConfig" const val SERVER_CUSTOM = "serverCustom" @@ -98,6 +97,7 @@ object Key { const val SERVER_SECURITY_CATEGORY = "serverSecurityCategory" const val SERVER_TLS_CAMOUFLAGE_CATEGORY = "serverTlsCamouflageCategory" + const val SERVER_ECH_CATEORY = "serverECHCategory" const val SERVER_WS_CATEGORY = "serverWsCategory" const val SERVER_SS_CATEGORY = "serverSsCategory" const val SERVER_HEADERS = "serverHeaders" @@ -108,19 +108,16 @@ object Key { const val SERVER_DOWNLOAD_SPEED = "serverDownloadSpeed" const val SERVER_STREAM_RECEIVE_WINDOW = "serverStreamReceiveWindow" const val SERVER_CONNECTION_RECEIVE_WINDOW = "serverConnectionReceiveWindow" - const val SERVER_MTU = "serverMTU" const val SERVER_DISABLE_MTU_DISCOVERY = "serverDisableMtuDiscovery" const val SERVER_HOP_INTERVAL = "hopInterval" const val SERVER_PRIVATE_KEY = "serverPrivateKey" - const val SERVER_LOCAL_ADDRESS = "serverLocalAddress" const val SERVER_INSECURE_CONCURRENCY = "serverInsecureConcurrency" const val SERVER_UDP_RELAY_MODE = "serverUDPRelayMode" const val SERVER_CONGESTION_CONTROLLER = "serverCongestionController" const val SERVER_DISABLE_SNI = "serverDisableSNI" const val SERVER_REDUCE_RTT = "serverReduceRTT" - const val SERVER_FAST_CONNECT = "serverFastConnect" const val ROUTE_NAME = "routeName" const val ROUTE_DOMAIN = "routeDomain" @@ -152,7 +149,6 @@ object Key { // - const val NEKO_PLUGIN_MANAGED = "nekoPlugins" const val APP_TLS_VERSION = "appTLSVersion" const val ENABLE_CLASH_API = "enableClashAPI" } @@ -160,6 +156,7 @@ object Key { object TunImplementation { const val GVISOR = 0 const val SYSTEM = 1 + const val MIXED = 2 } object IPv6Mode { diff --git a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt index c082ec3db..f38305d46 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt @@ -6,8 +6,6 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.pm.PackageInfo -import android.content.pm.PackageManager import android.content.res.Configuration import android.net.ConnectivityManager import android.net.Network @@ -22,22 +20,23 @@ import go.Seq import io.nekohasekai.sagernet.bg.SagerConnection import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.isOss +import io.nekohasekai.sagernet.ktx.isPreview import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.ui.MainActivity import io.nekohasekai.sagernet.utils.* import kotlinx.coroutines.DEBUG_PROPERTY_NAME import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON -import libcore.BoxPlatformInterface import libcore.Libcore -import libcore.NB4AInterface +import moe.matsuri.nb4a.NativeInterface +import moe.matsuri.nb4a.net.LocalResolverImpl import moe.matsuri.nb4a.utils.JavaUtil import moe.matsuri.nb4a.utils.cleanWebview -import java.net.InetSocketAddress +import java.io.File import androidx.work.Configuration as WorkConfiguration class SagerNet : Application(), - BoxPlatformInterface, - WorkConfiguration.Provider, NB4AInterface { + WorkConfiguration.Provider { override fun attachBaseContext(base: Context) { super.attachBaseContext(base) @@ -45,18 +44,31 @@ class SagerNet : Application(), application = this } - val externalAssets by lazy { getExternalFilesDir(null) ?: filesDir } - val process = JavaUtil.getProcessName() - val isMainProcess = process == BuildConfig.APPLICATION_ID + private val nativeInterface = NativeInterface() + + val externalAssets: File by lazy { getExternalFilesDir(null) ?: filesDir } + val process: String = JavaUtil.getProcessName() + private val isMainProcess = process == BuildConfig.APPLICATION_ID val isBgProcess = process.endsWith(":bg") override fun onCreate() { super.onCreate() - System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) Thread.setDefaultUncaughtExceptionHandler(CrashHandler) if (isMainProcess || isBgProcess) { + externalAssets.mkdirs() + Seq.setContext(this) + Libcore.initCore( + process, + cacheDir.absolutePath + "/", + filesDir.absolutePath + "/", + externalAssets.absolutePath + "/", + DataStore.logBufSize, + DataStore.logLevel > 0, + nativeInterface, nativeInterface, LocalResolverImpl + ) + // fix multi process issue in Android 9+ JavaUtil.handleWebviewDir(this) @@ -66,24 +78,6 @@ class SagerNet : Application(), } } - Seq.setContext(this) - updateNotificationChannels() - - // nb4a: init core - externalAssets.mkdirs() - Libcore.initCore( - process, - cacheDir.absolutePath + "/", - filesDir.absolutePath + "/", - externalAssets.absolutePath + "/", - DataStore.logBufSize, - DataStore.logLevel > 0, - this - ) - - // libbox: platform interface - Libcore.setBoxPlatformInterface(this) - if (isMainProcess) { Theme.apply(this) Theme.applyNightTheme() @@ -91,24 +85,24 @@ class SagerNet : Application(), DefaultNetworkListener.start(this) { underlyingNetwork = it } + + updateNotificationChannels() } } - if (BuildConfig.DEBUG) StrictMode.setVmPolicy( - StrictMode.VmPolicy.Builder() - .detectLeakedSqlLiteObjects() - .detectLeakedClosableObjects() - .detectLeakedRegistrationObjects() - .penaltyLog() - .build() - ) + if (BuildConfig.DEBUG) { + System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) + StrictMode.setVmPolicy( + StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .penaltyLog() + .build() + ) + } } - fun getPackageInfo(packageName: String) = packageManager.getPackageInfo( - packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES - else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES - )!! - override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateNotificationChannels() @@ -135,11 +129,6 @@ class SagerNet : Application(), uiMode.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION } - // /data/user_de available when not unlocked - val deviceStorage by lazy { - if (Build.VERSION.SDK_INT < 24) application else DeviceStorageApp(application) - } - val configureIntent: (Context) -> PendingIntent by lazy { { PendingIntent.getActivity( @@ -160,8 +149,6 @@ class SagerNet : Application(), val uiMode by lazy { application.getSystemService()!! } val power by lazy { application.getSystemService()!! } - val packageInfo: PackageInfo by lazy { application.getPackageInfo(application.packageName) } - fun getClipboardText(): String { return clipboard.primaryClip?.takeIf { it.itemCount > 0 } ?.getItemAt(0)?.text?.toString() ?: "" @@ -193,6 +180,10 @@ class SagerNet : Application(), "service-subscription", application.getText(R.string.service_subscription), NotificationManager.IMPORTANCE_DEFAULT + ), NotificationChannel( + "connection-test", + application.getText(R.string.connection_test), + NotificationManager.IMPORTANCE_DEFAULT ) ) ) @@ -211,59 +202,18 @@ class SagerNet : Application(), var underlyingNetwork: Network? = null - } - - - // libbox interface - - override fun autoDetectInterfaceControl(fd: Int) { - DataStore.vpnService?.protect(fd) - } - - override fun openTun(singTunOptionsJson: String, tunPlatformOptionsJson: String): Long { - if (DataStore.vpnService == null) { - throw Exception("no VpnService") - } - return DataStore.vpnService!!.startVpn(singTunOptionsJson, tunPlatformOptionsJson).toLong() - } - - override fun useProcFS(): Boolean { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q - } - - @RequiresApi(Build.VERSION_CODES.Q) - override fun findConnectionOwner( - ipProto: Int, srcIp: String, srcPort: Int, destIp: String, destPort: Int - ): Int { - return connectivity.getConnectionOwnerUid( - ipProto, InetSocketAddress(srcIp, srcPort), InetSocketAddress(destIp, destPort) - ) - } - - override fun packageNameByUid(uid: Int): String { - PackageCache.awaitLoadSync() - - if (uid <= 1000L) { - return "android" - } - - val packageNames = PackageCache.uidMap[uid] - if (!packageNames.isNullOrEmpty()) for (packageName in packageNames) { - return packageName - } - - error("unknown uid $uid") - } - - override fun uidByPackageName(packageName: String): Int { - PackageCache.awaitLoadSync() - return PackageCache[packageName] ?: 0 - } - - // nb4a interface - - override fun useOfficialAssets(): Boolean { - return DataStore.rulesProvider == 0 + var appVersionNameForDisplay = { + var n = BuildConfig.VERSION_NAME + if (isPreview) { + n += " " + BuildConfig.PRE_VERSION_NAME + } else if (!isOss) { + n += " ${BuildConfig.FLAVOR}" + } + if (BuildConfig.DEBUG) { + n += " DEBUG" + } + n + }() } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt index d14a3c28d..e760983dc 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt @@ -7,6 +7,7 @@ import android.content.IntentFilter import android.os.* import android.widget.Toast import io.nekohasekai.sagernet.Action +import io.nekohasekai.sagernet.BootReceiver import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.aidl.ISagerNetService @@ -50,6 +51,19 @@ class BaseService { Intent.ACTION_SHUTDOWN -> service.persistStats() Action.RELOAD -> service.reload() // Action.SWITCH_WAKE_LOCK -> runOnDefaultDispatcher { service.switchWakeLock() } + PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (SagerNet.power.isDeviceIdleMode) { + proxy?.box?.sleep() + } else { + proxy?.box?.wake() + if (DataStore.wakeResetConnections) { + Libcore.resetAllConnections(true) + } + } + } + } + Action.RESET_UPSTREAM_CONNECTIONS -> runOnDefaultDispatcher { Libcore.resetAllConnections(true) runOnMainDispatcher { @@ -58,6 +72,7 @@ class BaseService { .show() } } + else -> service.stopRunner() } } @@ -68,8 +83,9 @@ class BaseService { fun changeState(s: State, msg: String? = null) { if (state == s && msg == null) return - binder.stateChanged(s, msg) state = s + DataStore.serviceState = s + binder.stateChanged(s, msg) } } @@ -86,9 +102,13 @@ class BaseService { override val coroutineContext = Dispatchers.Main.immediate + Job() override fun getState(): Int = (data?.state ?: State.Idle).ordinal - override fun getProfileName(): String = data?.proxy?.profile?.displayName() ?: "Idle" + override fun getProfileName(): String = data?.proxy?.displayProfileName ?: "Idle" override fun registerCallback(cb: ISagerNetServiceCallback, id: Int) { + if (id == SagerConnection.CONNECTION_ID_RESTART_BG) { + Runtime.getRuntime().exit(0) + return + } if (!callbackIdMap.contains(cb)) { callbacks.register(cb) } @@ -165,12 +185,10 @@ class BaseService { val ent = SagerDatabase.proxyDao.getById(DataStore.selectedProxy) val tag = data.proxy!!.config.profileTagMap[ent?.id] ?: "" if (tag.isNotBlank() && ent != null) { - val success = data.proxy!!.box.selectOutbound(tag) - if (success) runOnDefaultDispatcher { - data.proxy!!.looper?.selectMain(ent.id) - val title = ServiceNotification.genTitle(ent) - data.notification?.postNotificationTitle(title) - } + // select from GUI + data.proxy!!.box.selectOutbound(tag) + // or select from webui + // => selector_OnProxySelected } return } @@ -189,7 +207,6 @@ class BaseService { tmpBox.buildConfigTmp() if (tmpBox.lastSelectorGroupId == data.proxy?.lastSelectorGroupId) { return true - // TODO if profile changed? } return false } @@ -248,7 +265,7 @@ class BaseService { } } - open fun persistStats() { + fun persistStats() { // TODO NEW save app stats? } @@ -267,7 +284,9 @@ class BaseService { } if (oldName != null && upstreamInterfaceName != null && oldName != upstreamInterfaceName) { Logs.d("Network changed: $oldName -> $upstreamInterfaceName") - Libcore.resetAllConnections(true) + if (DataStore.networkChangeResetConnections) { + Libcore.resetAllConnections(true) + } } } } @@ -275,16 +294,6 @@ class BaseService { var wakeLock: PowerManager.WakeLock? fun acquireWakeLock() - suspend fun switchWakeLock() { - wakeLock?.apply { - release() - wakeLock = null - data.notification?.postNotificationWakeLockStatus(false) - } ?: apply { - acquireWakeLock() - data.notification?.postNotificationWakeLockStatus(true) - } - } suspend fun lateInit() { wakeLock?.apply { @@ -315,14 +324,34 @@ class BaseService { val proxy = ProxyInstance(profile, this) data.proxy = proxy + BootReceiver.enabled = DataStore.persistAcrossReboot if (!data.closeReceiverRegistered) { - registerReceiver(data.receiver, IntentFilter().apply { + val filter = IntentFilter().apply { addAction(Action.RELOAD) addAction(Intent.ACTION_SHUTDOWN) addAction(Action.CLOSE) // addAction(Action.SWITCH_WAKE_LOCK) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED) + } addAction(Action.RESET_UPSTREAM_CONNECTIONS) - }, "$packageName.SERVICE", null) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver( + data.receiver, + filter, + "$packageName.SERVICE", + null, + Context.RECEIVER_EXPORTED + ) + } else { + registerReceiver( + data.receiver, + filter, + "$packageName.SERVICE", + null + ) + } data.closeReceiverRegistered = true } @@ -344,19 +373,13 @@ class BaseService { startProcesses() data.changeState(State.Connected) - for ((type, routeName) in proxy.config.alerts) { - data.binder.broadcast { - it.routeAlert(type, routeName) - } - } - lateInit() } 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 (e: PluginManager.PluginNotFoundException) { Toast.makeText(this@Interface, e.readableMessage, Toast.LENGTH_SHORT).show() - Logs.d(e.readableMessage) + Logs.w(e) data.binder.missingPlugin(e.plugin) stopRunner(false, null) } catch (exc: Throwable) { diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt index 8e69f0b6a..5b860b35f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt @@ -3,24 +3,21 @@ package io.nekohasekai.sagernet.bg import android.system.ErrnoException import android.system.Os import android.system.OsConstants -import android.text.TextUtils import io.nekohasekai.sagernet.ktx.Logs import java.io.File import java.io.IOException +import androidx.core.text.isDigitsOnly object Executable { private val EXECUTABLES = setOf( - "libtrojan.so", - "libtrojan-go.so", - "libnaive.so", - "libhysteria.so", - "libwg.so" + "libtrojan.so", "libtrojan-go.so", "libnaive.so", "libtuic.so", "libhysteria.so" ) fun killAll(alsoKillBg: Boolean = false) { - for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) } - ?: return) { - val exe = File(try { + // kill bg may fail + for (process in File("/proc").listFiles { _, name -> name.isDigitsOnly() } ?: return) { + val exe = File( + try { File(process, "cmdline").inputStream().bufferedReader().use { it.readText() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt index db7000018..cded1cabb 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt @@ -64,6 +64,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException( "$cmdName exits too fast (exit code: $exitCode)" ) + exitCode == 128 + OsConstants.SIGKILL -> Logs.w("$cmdName was killed") else -> Logs.w(IOException("$cmdName unexpectedly exits with code $exitCode")) } @@ -99,6 +100,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C } override val coroutineContext = Dispatchers.Main.immediate + Job() + var processCount = 0 @MainThread fun start( @@ -111,6 +113,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C start() // if start fails, IOException will be thrown directly launch { looper(onRestartCallback) } } + processCount += 1 } @MainThread diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt index 0ddf8de08..97ff4b8a0 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt @@ -24,7 +24,7 @@ class SagerConnection( val serviceClass get() = when (DataStore.serviceMode) { Key.MODE_PROXY -> ProxyService::class - Key.MODE_VPN -> VpnService::class // Key.MODE_TRANS -> TransproxyService::class + Key.MODE_VPN -> VpnService::class else -> throw UnknownError() }.java @@ -32,6 +32,9 @@ class SagerConnection( const val CONNECTION_ID_TILE = 1 const val CONNECTION_ID_MAIN_ACTIVITY_FOREGROUND = 2 const val CONNECTION_ID_MAIN_ACTIVITY_BACKGROUND = 3 + const val CONNECTION_ID_RESTART_BG = 4 + + var restartingApp = false } interface Callback { @@ -39,11 +42,11 @@ class SagerConnection( fun cbSpeedUpdate(stats: SpeedDisplayData) {} fun cbTrafficUpdate(data: TrafficData) {} + fun cbSelectorUpdate(id: Long) {} fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) fun missingPlugin(profileName: String, pluginName: String) {} - fun routeAlert(type: Int, routeName: String) {} fun onServiceConnected(service: ISagerNetService) @@ -83,17 +86,17 @@ class SagerConnection( } } - override fun missingPlugin(profileName: String, pluginName: String) { + override fun cbSelectorUpdate(id: Long) { val callback = callback ?: return runOnMainDispatcher { - callback.missingPlugin(profileName, pluginName) + callback.cbSelectorUpdate(id) } } - override fun routeAlert(type: Int, routeName: String) { + override fun missingPlugin(profileName: String, pluginName: String) { val callback = callback ?: return runOnMainDispatcher { - callback.routeAlert(type, routeName) + callback.missingPlugin(profileName, pluginName) } } @@ -105,7 +108,11 @@ class SagerConnection( fun updateConnectionId(id: Int) { connectionId = id - service?.registerCallback(serviceCallback, id) + try { + service?.registerCallback(serviceCallback, id) + } catch (e: Exception) { + e.printStackTrace() + } } override fun onServiceConnected(name: ComponentName?, binder: IBinder) { @@ -120,7 +127,7 @@ class SagerConnection( } catch (e: RemoteException) { e.printStackTrace() } - callback!!.onServiceConnected(service) + callback?.onServiceConnected(service) } override fun onServiceDisconnected(name: ComponentName?) { @@ -133,7 +140,9 @@ class SagerConnection( override fun binderDied() { service = null callbackRegistered = false - callback?.also { runOnMainDispatcher { it.onBinderDied() } } + if (!restartingApp) { + callback?.also { runOnMainDispatcher { it.onBinderDied() } } + } } private fun unregisterCallback() { @@ -145,7 +154,7 @@ class SagerConnection( callbackRegistered = false } - fun connect(context: Context, callback: Callback) { + fun connect(context: Context, callback: Callback?) { if (connectionActive) return connectionActive = true check(this.callback == null) diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt index dce4b2105..0ac84ac6a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt @@ -6,8 +6,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED import android.os.Build import android.text.format.Formatter +import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.nekohasekai.sagernet.Action @@ -19,8 +21,11 @@ import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.database.SagerDatabase import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ktx.getColorAttr +import io.nekohasekai.sagernet.ktx.runOnMainDispatcher import io.nekohasekai.sagernet.ui.SwitchActivity import io.nekohasekai.sagernet.utils.Theme +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * User can customize visibility of notification since Android 8. @@ -48,8 +53,10 @@ class ServiceNotification( } } - fun postNotificationSpeedUpdate(stats: SpeedDisplayData) { - builder.apply { + var listenPostSpeed = true + + suspend fun postNotificationSpeedUpdate(stats: SpeedDisplayData) { + useBuilder { if (showDirectSpeed) { val speedDetail = (service as Context).getString( R.string.speed_detail, service.getString( @@ -64,8 +71,8 @@ class ServiceNotification( Formatter.formatFileSize(service, stats.rxRateDirect) ) ) - setStyle(NotificationCompat.BigTextStyle().bigText(speedDetail)) - setContentText(speedDetail) + it.setStyle(NotificationCompat.BigTextStyle().bigText(speedDetail)) + it.setContentText(speedDetail) } else { val speedSimple = (service as Context).getString( R.string.traffic, service.getString( @@ -74,9 +81,9 @@ class ServiceNotification( R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy) ) ) - setContentText(speedSimple) + it.setContentText(speedSimple) } - setSubText( + it.setSubText( service.getString( R.string.traffic, Formatter.formatFileSize(service, stats.txTotal), @@ -87,15 +94,19 @@ class ServiceNotification( update() } - fun postNotificationTitle(newTitle: String) { - builder.setContentTitle(newTitle) + suspend fun postNotificationTitle(newTitle: String) { + useBuilder { + it.setContentTitle(newTitle) + } update() } - fun postNotificationWakeLockStatus(acquired: Boolean) { + suspend fun postNotificationWakeLockStatus(acquired: Boolean) { updateActions() - builder.priority = - if (acquired) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_LOW + useBuilder { + it.priority = + if (acquired) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_LOW + } update() } @@ -111,79 +122,100 @@ class ServiceNotification( .setCategory(NotificationCompat.CATEGORY_SERVICE) .setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN) + private val buildLock = Mutex() + + private suspend fun useBuilder(f: (NotificationCompat.Builder) -> Unit) { + buildLock.withLock { + f(builder) + } + } + init { service as Context - updateActions() Theme.apply(app) Theme.apply(service) builder.color = service.getColorAttr(R.attr.colorPrimary) - updateCallback(SagerNet.power.isInteractive) service.registerReceiver(this, IntentFilter().apply { addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_OFF) }) - show() + + runOnMainDispatcher { + updateActions() + show() + } } - private fun updateActions() { + private suspend fun updateActions() { service as Context - builder.clearActions() + useBuilder { + it.clearActions() - val closeAction = NotificationCompat.Action.Builder( - 0, service.getText(R.string.stop), PendingIntent.getBroadcast( - service, 0, Intent(Action.CLOSE).setPackage(service.packageName), flags - ) - ).setShowsUserInterface(false).build() - builder.addAction(closeAction) + val closeAction = NotificationCompat.Action.Builder( + 0, service.getText(R.string.stop), PendingIntent.getBroadcast( + service, 0, Intent(Action.CLOSE).setPackage(service.packageName), flags + ) + ).setShowsUserInterface(false).build() + it.addAction(closeAction) - val switchAction = NotificationCompat.Action.Builder( - 0, service.getString(R.string.action_switch), PendingIntent.getActivity( - service, 0, Intent(service, SwitchActivity::class.java), flags - ) - ).setShowsUserInterface(false).build() - builder.addAction(switchAction) + val switchAction = NotificationCompat.Action.Builder( + 0, service.getString(R.string.action_switch), PendingIntent.getActivity( + service, 0, Intent(service, SwitchActivity::class.java), flags + ) + ).setShowsUserInterface(false).build() + it.addAction(switchAction) - val resetUpstreamAction = NotificationCompat.Action.Builder( - 0, service.getString(R.string.reset_connections), - PendingIntent.getBroadcast( - service, 0, Intent(Action.RESET_UPSTREAM_CONNECTIONS), flags - ) - ).setShowsUserInterface(false).build() - builder.addAction(resetUpstreamAction) - -// val wakeLockAction = NotificationCompat.Action.Builder( -// 0, -// service.getText(if (!wakeLockAcquired) R.string.acquire_wake_lock else R.string.release_wake_lock), -// PendingIntent.getBroadcast( -// service, -// 0, -// Intent(Action.SWITCH_WAKE_LOCK).setPackage(service.packageName), -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 -// ) -// ).setShowsUserInterface(false).build() -// builder.addAction(wakeLockAction) + val resetUpstreamAction = NotificationCompat.Action.Builder( + 0, service.getString(R.string.reset_connections), + PendingIntent.getBroadcast( + service, 0, Intent(Action.RESET_UPSTREAM_CONNECTIONS), flags + ) + ).setShowsUserInterface(false).build() + it.addAction(resetUpstreamAction) + } } override fun onReceive(context: Context, intent: Intent) { - if (service.data.state == BaseService.State.Connected) updateCallback(intent.action == Intent.ACTION_SCREEN_ON) + if (service.data.state == BaseService.State.Connected) { + listenPostSpeed = intent.action == Intent.ACTION_SCREEN_ON + } } - var listenPostSpeed = false - private fun updateCallback(screenOn: Boolean) { - if (DataStore.speedInterval == 0) return - listenPostSpeed = screenOn - } + private suspend fun show() = + useBuilder { + try { + if (Build.VERSION.SDK_INT >= 34) { + (service as Service).startForeground( + notificationId, + it.build(), + FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED + ) + } else { + (service as Service).startForeground(notificationId, it.build()) + } + } catch (e: Exception) { + Toast.makeText( + SagerNet.application, + "startForeground: $e", + Toast.LENGTH_LONG + ).show() + } + } - private fun show() = (service as Service).startForeground(notificationId, builder.build()) - private fun update() = - NotificationManagerCompat.from(service as Service).notify(notificationId, builder.build()) + private suspend fun update() = useBuilder { + NotificationManagerCompat.from(service as Service).notify(notificationId, it.build()) + } fun destroy() { - (service as Service).stopForeground(true) + listenPostSpeed = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + (service as Service).stopForeground(Service.STOP_FOREGROUND_REMOVE) + } else { + (service as Service).stopForeground(true) + } service.unregisterReceiver(this) - updateCallback(false) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt index 5a367ee74..a00067283 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.work.CoroutineWorker -import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import androidx.work.PeriodicWorkRequest import androidx.work.WorkerParameters import androidx.work.multiprocess.RemoteWorkManager @@ -39,7 +39,7 @@ object SubscriptionUpdater { // main process RemoteWorkManager.getInstance(app).enqueueUniquePeriodicWork( WORK_NAME, - ExistingPeriodicWorkPolicy.REPLACE, + UPDATE, PeriodicWorkRequest.Builder(UpdateTask::class.java, minDelay, TimeUnit.MINUTES) .apply { if (minInitDelay > 0) setInitialDelay(minInitDelay, TimeUnit.SECONDS) diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt index 54239e914..dd46ee5cd 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt @@ -1,23 +1,3 @@ -/******************************************************************************* - * * - * Copyright (C) 2017 by Max Lv * - * Copyright (C) 2017 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.bg import android.graphics.drawable.Icon @@ -26,6 +6,7 @@ import androidx.annotation.RequiresApi import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.aidl.ISagerNetService +import io.nekohasekai.sagernet.database.SagerDatabase import android.service.quicksettings.TileService as BaseTileService @RequiresApi(24) @@ -39,16 +20,21 @@ class TileService : BaseTileService(), SagerConnection.Callback { private val connection = SagerConnection(SagerConnection.CONNECTION_ID_TILE) override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = - updateTile(state) { profileName } + updateTile(state, profileName) override fun onServiceConnected(service: ISagerNetService) { - updateTile(BaseService.State.values()[service.state]) { service.profileName } + updateTile(BaseService.State.values()[service.state], service.profileName) if (tapPending) { tapPending = false onClick() } } + override fun cbSelectorUpdate(id: Long) { + val profile = SagerDatabase.proxyDao.getById(id) ?: return + updateTile(BaseService.State.Connected, profile.displayName()) + } + override fun onStartListening() { super.onStartListening() connection.connect(this, this) @@ -60,10 +46,10 @@ class TileService : BaseTileService(), SagerConnection.Callback { } override fun onClick() { - toggle() + if (isLocked) unlockAndRun(this::toggle) else toggle() } - private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) { + private fun updateTile(serviceState: BaseService.State, profileName: String?) { qsTile?.apply { label = null when (serviceState) { @@ -72,15 +58,18 @@ class TileService : BaseTileService(), SagerConnection.Callback { icon = iconBusy state = Tile.STATE_ACTIVE } + BaseService.State.Connected -> { icon = iconConnected - label = profileName() + label = profileName state = Tile.STATE_ACTIVE } + BaseService.State.Stopping -> { icon = iconBusy state = Tile.STATE_UNAVAILABLE } + BaseService.State.Stopped -> { icon = iconIdle state = Tile.STATE_INACTIVE diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt index b6f0b4376..751c14965 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt @@ -16,9 +16,6 @@ import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.ui.VpnRequestActivity import io.nekohasekai.sagernet.utils.Subnet -import libcore.* -import moe.matsuri.nb4a.net.LocalResolverImpl -import moe.matsuri.nb4a.proxy.neko.needBypassRootUid import android.net.VpnService as BaseVpnService class VpnService : BaseVpnService(), @@ -95,9 +92,8 @@ class VpnService : BaseVpnService(), // val tunOptions = JSONObject(tunOptionsJson) // address & route & MTU ...... use NB4A GUI config - val profile = data.proxy!!.profile val builder = Builder().setConfigureIntent(SagerNet.configureIntent(this)) - .setSession(profile.displayName()) + .setSession(getString(R.string.app_name)) .setMtu(DataStore.mtu) val ipv6Mode = DataStore.ipv6Mode @@ -132,11 +128,11 @@ class VpnService : BaseVpnService(), // app route val packageName = packageName - var proxyApps = DataStore.proxyApps + val proxyApps = DataStore.proxyApps var bypass = DataStore.bypass - var workaroundSYSTEM = false /* DataStore.tunImplementation == TunImplementation.SYSTEM */ - var needBypassRootUid = workaroundSYSTEM || data.proxy!!.config.trafficMap.values.any { - it[0].nekoBean?.needBypassRootUid() == true || it[0].hysteriaBean?.protocol == HysteriaBean.PROTOCOL_FAKETCP + val workaroundSYSTEM = false /* DataStore.tunImplementation == TunImplementation.SYSTEM */ + val needBypassRootUid = workaroundSYSTEM || data.proxy!!.config.trafficMap.values.any { + it[0].hysteriaBean?.protocol == HysteriaBean.PROTOCOL_FAKETCP } if (proxyApps || needBypassRootUid) { @@ -200,9 +196,6 @@ class VpnService : BaseVpnService(), if (Build.VERSION.SDK_INT >= 29) builder.setMetered(metered) conn = builder.establish() ?: throw NullConnectionException() - // post setup - Libcore.setLocalResolver(LocalResolverImpl) - return conn!!.fd } @@ -218,7 +211,6 @@ class VpnService : BaseVpnService(), override fun onRevoke() = stopRunner() override fun onDestroy() { - Libcore.setLocalResolver(null) DataStore.vpnService = null super.onDestroy() data.binder.close() diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt index 190a99e4f..9f16723de 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt @@ -1,6 +1,5 @@ package io.nekohasekai.sagernet.bg.proto -import android.os.Build import android.os.SystemClock import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.bg.AbstractInstance @@ -10,23 +9,19 @@ import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.fmt.ConfigBuildResult import io.nekohasekai.sagernet.fmt.buildConfig import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean -import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig +import io.nekohasekai.sagernet.fmt.hysteria.buildHysteria1Config +import io.nekohasekai.sagernet.fmt.mieru.MieruBean +import io.nekohasekai.sagernet.fmt.mieru.buildMieruConfig import io.nekohasekai.sagernet.fmt.naive.NaiveBean import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig -import io.nekohasekai.sagernet.fmt.tuic.TuicBean -import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.plugin.PluginManager import kotlinx.coroutines.* import libcore.BoxInstance import libcore.Libcore -import moe.matsuri.nb4a.plugin.NekoPluginManager -import moe.matsuri.nb4a.proxy.neko.NekoBean -import moe.matsuri.nb4a.proxy.neko.NekoJSInterface -import moe.matsuri.nb4a.proxy.neko.updateAllConfig -import org.json.JSONObject +import moe.matsuri.nb4a.net.LocalResolverImpl import java.io.File abstract class BoxInstance( @@ -54,8 +49,7 @@ abstract class BoxInstance( } protected open suspend fun loadConfig() { - NekoJSInterface.Default.destroyAllJsi() - box = Libcore.newSingBoxInstance(config.config) + box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl) } open suspend fun init() { @@ -67,13 +61,20 @@ abstract class BoxInstance( initPlugin("trojan-go-plugin") pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(port) } + + is MieruBean -> { + initPlugin("mieru-plugin") + pluginConfigs[port] = profile.type to bean.buildMieruConfig(port) + } + is NaiveBean -> { initPlugin("naive-plugin") pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port) } + is HysteriaBean -> { initPlugin("hysteria-plugin") - pluginConfigs[port] = profile.type to bean.buildHysteriaConfig(port) { + pluginConfigs[port] = profile.type to bean.buildHysteria1Config(port) { File( app.cacheDir, "hysteria_" + SystemClock.elapsedRealtime() + ".ca" ).apply { @@ -82,28 +83,6 @@ abstract class BoxInstance( } } } - is TuicBean -> { - initPlugin("tuic-plugin") - pluginConfigs[port] = profile.type to bean.buildTuicConfig(port) { - File( - app.noBackupFilesDir, - "tuic_" + SystemClock.elapsedRealtime() + ".ca" - ).apply { - parentFile?.mkdirs() - cacheFiles.add(this) - } - } - } - is NekoBean -> { - // check if plugin binary can be loaded - initPlugin(bean.plgId) - - // build config and check if succeed - bean.updateAllConfig(port) - if (bean.allConfig == null) { - throw NekoPluginManager.PluginInternalException(bean.protocolId) - } - } } } } @@ -112,10 +91,8 @@ abstract class BoxInstance( override fun launch() { // TODO move, this is not box - val context = - if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked) SagerNet.application else SagerNet.deviceStorage - val cache = File(context.cacheDir, "tmpcfg") - cache.mkdirs() + val cacheDir = File(SagerNet.application.cacheDir, "tmpcfg") + cacheDir.mkdirs() for ((chain) in config.externalIndex) { chain.entries.forEachIndexed { index, (port, profile) -> @@ -127,9 +104,10 @@ abstract class BoxInstance( externalInstances.containsKey(port) -> { externalInstances[port]!!.launch() } + bean is TrojanGoBean -> { val configFile = File( - cache, "trojan_go_" + SystemClock.elapsedRealtime() + ".json" + cacheDir, "trojan_go_" + SystemClock.elapsedRealtime() + ".json" ) configFile.parentFile?.mkdirs() configFile.writeText(config) @@ -141,9 +119,30 @@ abstract class BoxInstance( processes.start(commands) } + + bean is MieruBean -> { + val configFile = File( + cacheDir, "mieru_" + SystemClock.elapsedRealtime() + ".json" + ) + + configFile.parentFile?.mkdirs() + configFile.writeText(config) + cacheFiles.add(configFile) + + val envMap = mutableMapOf() + envMap["MIERU_CONFIG_JSON_FILE"] = configFile.absolutePath + envMap["MIERU_PROTECT_PATH"] = "protect_path" + + val commands = mutableListOf( + initPlugin("mieru-plugin").path, "run", + ) + + processes.start(commands, envMap) + } + bean is NaiveBean -> { val configFile = File( - cache, "naive_" + SystemClock.elapsedRealtime() + ".json" + cacheDir, "naive_" + SystemClock.elapsedRealtime() + ".json" ) configFile.parentFile?.mkdirs() @@ -154,7 +153,7 @@ abstract class BoxInstance( if (bean.certificates.isNotBlank()) { val certFile = File( - cache, "naive_" + SystemClock.elapsedRealtime() + ".crt" + cacheDir, "naive_" + SystemClock.elapsedRealtime() + ".crt" ) certFile.parentFile?.mkdirs() @@ -170,9 +169,10 @@ abstract class BoxInstance( processes.start(commands, envMap) } + bean is HysteriaBean -> { val configFile = File( - cache, "hysteria_" + SystemClock.elapsedRealtime() + ".json" + cacheDir, "hysteria_" + SystemClock.elapsedRealtime() + ".json" ) configFile.parentFile?.mkdirs() @@ -193,61 +193,6 @@ abstract class BoxInstance( commands.addAll(0, listOf("su", "-c")) } - processes.start(commands) - } - bean is NekoBean -> { - // config built from JS - val nekoRunConfigs = bean.allConfig.optJSONArray("nekoRunConfigs") - val configs = mutableMapOf() - - nekoRunConfigs?.forEach { _, any -> - any as JSONObject - - val name = any.getString("name") - val configFile = File(cache, name) - configFile.parentFile?.mkdirs() - val content = any.getString("content") - configFile.writeText(content) - - cacheFiles.add(configFile) - configs[name] = configFile.absolutePath - - Logs.d(name + "\n\n" + content) - } - - val nekoCommands = bean.allConfig.getJSONArray("nekoCommands") - val commands = mutableListOf() - - nekoCommands.forEach { _, any -> - if (any is String) { - if (configs.containsKey(any)) { - commands.add(configs[any]!!) - } else if (any == "%exe%") { - commands.add(initPlugin(bean.plgId).path) - } else { - commands.add(any) - } - } - } - - processes.start(commands) - } - bean is TuicBean -> { - val configFile = File( - context.noBackupFilesDir, - "tuic_" + SystemClock.elapsedRealtime() + ".json" - ) - - configFile.parentFile?.mkdirs() - configFile.writeText(config) - cacheFiles.add(configFile) - - val commands = mutableListOf( - initPlugin("tuic-plugin").path, - "-c", - configFile.absolutePath, - ) - processes.start(commands) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt index 81178fdbc..9758a5c40 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt @@ -2,6 +2,7 @@ package io.nekohasekai.sagernet.bg.proto import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.bg.BaseService +import io.nekohasekai.sagernet.bg.ServiceNotification import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher @@ -11,9 +12,11 @@ import moe.matsuri.nb4a.utils.JavaUtil class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = null) : BoxInstance(profile) { - var lastSelectorGroupId = -1L var notTmp = true + var lastSelectorGroupId = -1L + var displayProfileName = ServiceNotification.genTitle(profile) + // for TrafficLooper var looper: TrafficLooper? = null @@ -39,9 +42,13 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = } } + override suspend fun loadConfig() { + super.loadConfig() + } + override fun launch() { box.setAsMain() - super.launch() + super.launch() // start box runOnDefaultDispatcher { looper = service?.let { TrafficLooper(it.data, this) } looper?.start() diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt index e4b85df0f..bc6907137 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt @@ -8,10 +8,12 @@ import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.ktx.tryResume import io.nekohasekai.sagernet.ktx.tryResumeWithException +import kotlinx.coroutines.delay import libcore.Libcore +import moe.matsuri.nb4a.net.LocalResolverImpl import kotlin.coroutines.suspendCoroutine -class TestInstance(profile: ProxyEntity, val link: String, val timeout: Int) : +class TestInstance(profile: ProxyEntity, val link: String, private val timeout: Int) : BoxInstance(profile) { suspend fun doTest(): Int { @@ -25,6 +27,10 @@ class TestInstance(profile: ProxyEntity, val link: String, val timeout: Int) : try { init() launch() + if (processes.processCount > 0) { + // wait for plugin start + delay(500) + } c.tryResume(Libcore.urlTest(box, link, timeout)) } catch (e: Exception) { c.tryResumeWithException(e) @@ -41,8 +47,7 @@ class TestInstance(profile: ProxyEntity, val link: String, val timeout: Int) : override suspend fun loadConfig() { // don't call destroyAllJsi here if (BuildConfig.DEBUG) Logs.d(config.config) - box = Libcore.newSingBoxInstance(config.config) - box.forTest = true + box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt index 98bfbdda4..2d4bf7323 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt @@ -9,6 +9,7 @@ import io.nekohasekai.sagernet.database.ProfileManager import io.nekohasekai.sagernet.fmt.TAG_BYPASS import io.nekohasekai.sagernet.fmt.TAG_PROXY import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import kotlinx.coroutines.* class TrafficLooper @@ -17,7 +18,8 @@ class TrafficLooper ) { private var job: Job? = null - private val items = mutableMapOf() // associate ent id + private val idMap = mutableMapOf() // id to 1 data + private val tagMap = mutableMapOf() // tag to 1 data suspend fun stop() { job?.cancel() @@ -26,7 +28,7 @@ class TrafficLooper val traffic = mutableMapOf() data.proxy?.config?.trafficMap?.forEach { (_, ents) -> for (ent in ents) { - val item = items[ent.id] ?: return@forEach + val item = idMap[ent.id] ?: return@forEach ent.rx = item.rx ent.tx = item.tx ProfileManager.updateProfile(ent) // update DB @@ -54,15 +56,25 @@ class TrafficLooper fun selectMain(id: Long) { Logs.d("select traffic count $TAG_PROXY to $id, old id is $selectorNowId") - val oldData = items[selectorNowId] - val data = items[id] ?: return + val oldData = idMap[selectorNowId] + val newData = idMap[id] ?: return oldData?.apply { tag = selectorNowFakeTag ignore = true + // post traffic when switch + if (DataStore.profileTrafficStatistics) { + data.proxy?.config?.trafficMap?.get(tag)?.firstOrNull()?.let { + it.rx = rx + it.tx = tx + runOnDefaultDispatcher { + ProfileManager.updateProfile(it) // update DB + } + } + } } - selectorNowFakeTag = data.tag + selectorNowFakeTag = newData.tag selectorNowId = id - data.apply { + newData.apply { tag = TAG_PROXY ignore = false } @@ -78,19 +90,19 @@ class TrafficLooper var proxy: ProxyInstance? // for display - var itemMain: TrafficUpdater.TrafficLooperData? = null - var itemMainBase: TrafficUpdater.TrafficLooperData? = null - var itemBypass: TrafficUpdater.TrafficLooperData? = null + val itemBypass = TrafficUpdater.TrafficLooperData(tag = TAG_BYPASS) while (sc.isActive) { - delay(delayMs) - proxy = data.proxy ?: continue + proxy = data.proxy + if (proxy == null) { + delay(delayMs) + continue + } if (trafficUpdater == null) { if (!proxy.isInitialized()) continue - items.clear() - itemBypass = TrafficUpdater.TrafficLooperData(tag = "bypass") - items[-1] = itemBypass + idMap.clear() + idMap[-1] = itemBypass // val tags = hashSetOf(TAG_PROXY, TAG_BYPASS) proxy.config.trafficMap.forEach { (tag, ents) -> @@ -100,30 +112,21 @@ class TrafficLooper tag = tag, rx = ent.rx, tx = ent.tx, + rxBase = ent.rx, + txBase = ent.tx, ignore = proxy.config.selectorGroupId >= 0L, ) - if (tag == TAG_PROXY && itemMain == null) { - itemMain = item - itemMainBase = TrafficUpdater.TrafficLooperData( - tag = tag, - rx = ent.rx, - tx = ent.tx, - ) - Logs.d("traffic count $tag to main to ${ent.id}") - } - items[ent.id] = item + idMap[ent.id] = item + tagMap[tag] = item Logs.d("traffic count $tag to ${ent.id}") } } if (proxy.config.selectorGroupId >= 0L) { - itemMain = TrafficUpdater.TrafficLooperData(tag = TAG_PROXY) - itemMainBase = TrafficUpdater.TrafficLooperData(tag = TAG_PROXY) - items[-2] = itemMain!! selectMain(proxy.config.mainEntId) } // trafficUpdater = TrafficUpdater( - box = proxy.box, items = items.values.toList() + box = proxy.box, items = idMap.values.toList() ) proxy.box.setV2rayStats(tags.joinToString("\n")) } @@ -131,14 +134,28 @@ class TrafficLooper trafficUpdater.updateAll() if (!sc.isActive) return + // add all non-bypass to "main" + var mainTxRate = 0L + var mainRxRate = 0L + var mainTx = 0L + var mainRx = 0L + tagMap.forEach { (_, it) -> + if (!it.ignore) { + mainTxRate += it.txRate + mainRxRate += it.rxRate + } + mainTx += it.tx - it.txBase + mainRx += it.rx - it.rxBase + } + // speed val speed = SpeedDisplayData( - itemMain!!.txRate, - itemMain!!.rxRate, - if (showDirectSpeed) itemBypass!!.txRate else 0L, - if (showDirectSpeed) itemBypass!!.rxRate else 0L, - itemMain!!.tx - itemMainBase!!.tx, - itemMain!!.rx - itemMainBase!!.rx + mainTxRate, + mainRxRate, + if (showDirectSpeed) itemBypass.txRate else 0L, + if (showDirectSpeed) itemBypass.rxRate else 0L, + mainTx, + mainRx ) // broadcast (MainActivity) @@ -149,7 +166,7 @@ class TrafficLooper if (data.binder.callbackIdMap[b] == SagerConnection.CONNECTION_ID_MAIN_ACTIVITY_FOREGROUND) { b.cbSpeedUpdate(speed) if (profileTrafficStatistics) { - items.forEach { (id, item) -> + idMap.forEach { (id, item) -> b.cbTrafficUpdate( TrafficData(id = id, rx = item.rx, tx = item.tx) // display ) @@ -163,6 +180,8 @@ class TrafficLooper data.notification?.apply { if (listenPostSpeed) postNotificationSpeedUpdate(speed) } + + delay(delayMs) } } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficUpdater.kt index 3081e2b0c..c13869c5c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficUpdater.kt @@ -10,6 +10,8 @@ class TrafficUpdater( var tag: String, var tx: Long = 0, var rx: Long = 0, + var txBase: Long = 0, + var rxBase: Long = 0, var txRate: Long = 0, var rxRate: Long = 0, var lastUpdate: Long = 0, diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/UrlTest.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/UrlTest.kt index 3cec7cff4..73b654630 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/UrlTest.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/UrlTest.kt @@ -6,7 +6,7 @@ import io.nekohasekai.sagernet.database.ProxyEntity class UrlTest { val link = DataStore.connectionTestURL - val timeout = 3000 + private val timeout = 5000 suspend fun doTest(profile: ProxyEntity): Int { return TestInstance(profile, link, timeout).doTest() 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 c413ee686..06bb50133 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -2,18 +2,28 @@ package io.nekohasekai.sagernet.database import android.os.Binder import androidx.preference.PreferenceDataStore -import io.nekohasekai.sagernet.* +import io.nekohasekai.sagernet.CONNECTION_TEST_URL +import io.nekohasekai.sagernet.GroupType +import io.nekohasekai.sagernet.IPv6Mode +import io.nekohasekai.sagernet.Key +import io.nekohasekai.sagernet.TunImplementation import io.nekohasekai.sagernet.bg.BaseService import io.nekohasekai.sagernet.bg.VpnService import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener import io.nekohasekai.sagernet.database.preference.PublicDatabase import io.nekohasekai.sagernet.database.preference.RoomPreferenceDataStore -import io.nekohasekai.sagernet.ktx.* +import io.nekohasekai.sagernet.ktx.boolean +import io.nekohasekai.sagernet.ktx.int +import io.nekohasekai.sagernet.ktx.long +import io.nekohasekai.sagernet.ktx.parsePort +import io.nekohasekai.sagernet.ktx.string +import io.nekohasekai.sagernet.ktx.stringToInt +import io.nekohasekai.sagernet.ktx.stringToIntIfExists import moe.matsuri.nb4a.TempDatabase object DataStore : OnPreferenceDataStoreChangeListener { - // share service state in main process + // share service state in main & bg process @Volatile var serviceState = BaseService.State.Idle @@ -30,6 +40,10 @@ object DataStore : OnPreferenceDataStoreChangeListener { var vpnService: VpnService? = null var baseService: BaseService.Interface? = null + // main + + var runningTest = false + fun currentGroupId(): Long { val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1) if (currentSelected > 0L) return currentSelected @@ -70,11 +84,14 @@ object DataStore : OnPreferenceDataStoreChangeListener { return groups.find { it.type == GroupType.BASIC }!!.id } - var nekoPlugins by configurationStore.string(Key.NEKO_PLUGIN_MANAGED) var appTLSVersion by configurationStore.string(Key.APP_TLS_VERSION) var enableClashAPI by configurationStore.boolean(Key.ENABLE_CLASH_API) var showBottomBar by configurationStore.boolean(Key.SHOW_BOTTOM_BAR) + var allowInsecureOnRequest by configurationStore.boolean(Key.ALLOW_INSECURE_ON_REQUEST) + var networkChangeResetConnections by configurationStore.boolean(Key.NETWORK_CHANGE_RESET_CONNECTIONS) { true } + var wakeResetConnections by configurationStore.boolean(Key.WAKE_RESET_CONNECTIONS) + // var isExpert by configurationStore.boolean(Key.APP_EXPERT) @@ -82,11 +99,9 @@ object DataStore : OnPreferenceDataStoreChangeListener { var nightTheme by configurationStore.stringToInt(Key.NIGHT_THEME) var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN } - // var domainStrategy by configurationStore.string(Key.DOMAIN_STRATEGY) { "AsIs" } - var trafficSniffing by configurationStore.boolean(Key.TRAFFIC_SNIFFING) { true } + var trafficSniffing by configurationStore.stringToInt(Key.TRAFFIC_SNIFFING) { 1 } var resolveDestination by configurationStore.boolean(Key.RESOLVE_DESTINATION) - // var tcpKeepAliveInterval by configurationStore.stringToInt(Key.TCP_KEEP_ALIVE_INTERVAL) { 15 } var mtu by configurationStore.stringToInt(Key.MTU) { 9000 } var bypassLan by configurationStore.boolean(Key.BYPASS_LAN) @@ -96,12 +111,12 @@ object DataStore : OnPreferenceDataStoreChangeListener { var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL) var showGroupInNotification by configurationStore.boolean("showGroupInNotification") - var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://8.8.8.8/dns-query" } + var globalCustomConfig by configurationStore.string(Key.GLOBAL_CUSTOM_CONFIG) { "" } + + var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://dns.google/dns-query" } var directDns by configurationStore.string(Key.DIRECT_DNS) { "https://223.5.5.5/dns-query" } - var directDnsUseSystem by configurationStore.boolean(Key.DIRECT_DNS_USE_SYSTEM) var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING) { true } - var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS) - var dnsNetwork by configurationStore.stringSet(Key.DNS_NETWORK) + var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS) { true } var rulesProvider by configurationStore.stringToInt(Key.RULES_PROVIDER) var logLevel by configurationStore.stringToInt(Key.LOG_LEVEL) @@ -113,25 +128,11 @@ object DataStore : OnPreferenceDataStoreChangeListener { var mixedPort: Int get() = getLocalPort(Key.MIXED_PORT, 2080) set(value) = saveLocalPort(Key.MIXED_PORT, value) - var localDNSPort: Int - get() = getLocalPort(Key.LOCAL_DNS_PORT, 6450) - set(value) { - saveLocalPort(Key.LOCAL_DNS_PORT, value) - } - var transproxyPort: Int - get() = getLocalPort(Key.TRANSPROXY_PORT, 9200) - set(value) = saveLocalPort(Key.TRANSPROXY_PORT, value) fun initGlobal() { if (configurationStore.getString(Key.MIXED_PORT) == null) { mixedPort = mixedPort } - if (configurationStore.getString(Key.LOCAL_DNS_PORT) == null) { - localDNSPort = localDNSPort - } - if (configurationStore.getString(Key.TRANSPROXY_PORT) == null) { - transproxyPort = transproxyPort - } } @@ -151,22 +152,21 @@ object DataStore : OnPreferenceDataStoreChangeListener { var individual by configurationStore.string(Key.INDIVIDUAL) var showDirectSpeed by configurationStore.boolean(Key.SHOW_DIRECT_SPEED) { true } + val persistAcrossReboot by configurationStore.boolean(Key.PERSIST_ACROSS_REBOOT) { false } + var appendHttpProxy by configurationStore.boolean(Key.APPEND_HTTP_PROXY) - var requireTransproxy by configurationStore.boolean(Key.REQUIRE_TRANSPROXY) - var transproxyMode by configurationStore.stringToInt(Key.TRANSPROXY_MODE) var connectionTestURL by configurationStore.string(Key.CONNECTION_TEST_URL) { CONNECTION_TEST_URL } var connectionTestConcurrent by configurationStore.int("connectionTestConcurrent") { 5 } var alwaysShowAddress by configurationStore.boolean(Key.ALWAYS_SHOW_ADDRESS) - var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.SYSTEM } + var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.GVISOR } var profileTrafficStatistics by configurationStore.boolean(Key.PROFILE_TRAFFIC_STATISTICS) { true } var yacdURL by configurationStore.string("yacdURL") { "http://127.0.0.1:9090/ui" } // protocol - var muxProtocols by configurationStore.stringSet(Key.MUX_PROTOCOLS) - var muxConcurrency by configurationStore.stringToInt(Key.MUX_CONCURRENCY) { 8 } + var globalAllowInsecure by configurationStore.boolean(Key.GLOBAL_ALLOW_INSECURE) { false } // old cache, DO NOT ADD @@ -176,6 +176,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { var profileName by profileCacheStore.string(Key.PROFILE_NAME) var serverAddress by profileCacheStore.string(Key.SERVER_ADDRESS) var serverPort by profileCacheStore.stringToInt(Key.SERVER_PORT) + var serverPorts by profileCacheStore.string("serverPorts") var serverUsername by profileCacheStore.string(Key.SERVER_USERNAME) var serverPassword by profileCacheStore.string(Key.SERVER_PASSWORD) var serverPassword1 by profileCacheStore.string(Key.SERVER_PASSWORD1) @@ -193,6 +194,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { var serverEncryption by profileCacheStore.string(Key.SERVER_ENCRYPTION) var serverALPN by profileCacheStore.string(Key.SERVER_ALPN) var serverCertificates by profileCacheStore.string(Key.SERVER_CERTIFICATES) + var serverMTU by profileCacheStore.stringToInt(Key.SERVER_MTU) var serverHeaders by profileCacheStore.string(Key.SERVER_HEADERS) var serverAllowInsecure by profileCacheStore.boolean(Key.SERVER_ALLOW_INSECURE) @@ -201,11 +203,12 @@ object DataStore : OnPreferenceDataStoreChangeListener { var serverDownloadSpeed by profileCacheStore.stringToInt(Key.SERVER_DOWNLOAD_SPEED) var serverStreamReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_STREAM_RECEIVE_WINDOW) var serverConnectionReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_CONNECTION_RECEIVE_WINDOW) - var serverMTU by profileCacheStore.stringToInt(Key.SERVER_MTU) { 1420 } var serverDisableMtuDiscovery by profileCacheStore.boolean(Key.SERVER_DISABLE_MTU_DISCOVERY) var serverHopInterval by profileCacheStore.stringToInt(Key.SERVER_HOP_INTERVAL) { 10 } - var serverProtocolVersion by profileCacheStore.stringToInt(Key.SERVER_PROTOCOL) + var protocolVersion by profileCacheStore.stringToInt(Key.PROTOCOL_VERSION) { 2 } // default is SOCKS5 + + var serverProtocolInt by profileCacheStore.stringToInt(Key.SERVER_PROTOCOL) var serverPrivateKey by profileCacheStore.string(Key.SERVER_PRIVATE_KEY) var serverInsecureConcurrency by profileCacheStore.stringToInt(Key.SERVER_INSECURE_CONCURRENCY) @@ -213,7 +216,6 @@ object DataStore : OnPreferenceDataStoreChangeListener { var serverCongestionController by profileCacheStore.string(Key.SERVER_CONGESTION_CONTROLLER) var serverDisableSNI by profileCacheStore.boolean(Key.SERVER_DISABLE_SNI) var serverReduceRTT by profileCacheStore.boolean(Key.SERVER_REDUCE_RTT) - var serverFastConnect by profileCacheStore.boolean(Key.SERVER_FAST_CONNECT) var routeName by profileCacheStore.string(Key.ROUTE_NAME) var routeDomain by profileCacheStore.string(Key.ROUTE_DOMAIN) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt index 1bcad16d3..ba21dde44 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt @@ -17,7 +17,7 @@ object ProfileManager { interface Listener { suspend fun onAdd(profile: ProxyEntity) suspend fun onUpdated(data: TrafficData) - suspend fun onUpdated(profile: ProxyEntity) + suspend fun onUpdated(profile: ProxyEntity, noTraffic: Boolean) suspend fun onRemoved(groupId: Long, profileId: Long) } @@ -87,13 +87,13 @@ object ProfileManager { suspend fun updateProfile(profile: ProxyEntity) { SagerDatabase.proxyDao.updateProxy(profile) - iterator { onUpdated(profile) } + iterator { onUpdated(profile, false) } } suspend fun updateProfile(profiles: List) { SagerDatabase.proxyDao.updateProxy(profiles) profiles.forEach { - iterator { onUpdated(it) } + iterator { onUpdated(it, false) } } } @@ -141,12 +141,12 @@ object ProfileManager { // postUpdate: post to listeners, don't change the DB - suspend fun postUpdate(profileId: Long) { - postUpdate(getProfile(profileId) ?: return) + suspend fun postUpdate(profileId: Long, noTraffic: Boolean = false) { + postUpdate(getProfile(profileId) ?: return, noTraffic) } - suspend fun postUpdate(profile: ProxyEntity) { - iterator { onUpdated(profile) } + suspend fun postUpdate(profile: ProxyEntity, noTraffic: Boolean = false) { + iterator { onUpdated(profile, noTraffic) } } suspend fun postUpdate(data: TrafficData) { @@ -200,55 +200,37 @@ object ProfileManager { outbound = -2 ) ) - createRule( - RuleEntity( - name = app.getString(R.string.route_opt_block_analysis), - domains = app.assets.open("analysis.txt").use { - it.bufferedReader() - .readLines() - .filter { it.isNotBlank() } - .joinToString("\n") - }, - outbound = -2, - ) - ) - var country = Locale.getDefault().country.lowercase() - var displayCountry = Locale.getDefault().displayCountry - if (country in arrayOf( - "ir" - ) - ) { - createRule( + val fuckedCountry = mutableListOf("cn:中国") + if (Locale.getDefault().country != Locale.CHINA.country) { + // 非中文用户 + fuckedCountry += "ir:Iran" + fuckedCountry += "ru:Russia" + } + for (c in fuckedCountry) { + val country = c.substringBefore(":") + val displayCountry = c.substringAfter(":") + // + if (country == "cn") createRule( RuleEntity( - name = app.getString(R.string.route_bypass_domain, displayCountry), - domains = "domain:$country", - outbound = -1 + name = app.getString(R.string.route_play_store, displayCountry), + domains = "googleapis.cn", ), false ) - } else { - country = Locale.CHINA.country.lowercase() - displayCountry = Locale.CHINA.displayCountry createRule( RuleEntity( - name = app.getString(R.string.route_play_store, displayCountry), - domains = "domain:googleapis.cn", + name = app.getString(R.string.route_bypass_domain, displayCountry), + domains = "geosite:$country", + outbound = -1 ), false ) createRule( RuleEntity( - name = app.getString(R.string.route_bypass_domain, displayCountry), - domains = "geosite:$country", + name = app.getString(R.string.route_bypass_ip, displayCountry), + ip = "geoip:$country", outbound = -1 ), false ) } - createRule( - RuleEntity( - name = app.getString(R.string.route_bypass_ip, displayCountry), - ip = "geoip:$country", - outbound = -1 - ), false - ) rules = SagerDatabase.rulesDao.allRules() } return rules 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 551bf0e6d..b975695cb 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt @@ -11,6 +11,8 @@ import io.nekohasekai.sagernet.fmt.http.HttpBean import io.nekohasekai.sagernet.fmt.http.toUri import io.nekohasekai.sagernet.fmt.hysteria.* import io.nekohasekai.sagernet.fmt.internal.ChainBean +import io.nekohasekai.sagernet.fmt.mieru.MieruBean +import io.nekohasekai.sagernet.fmt.mieru.buildMieruConfig import io.nekohasekai.sagernet.fmt.naive.NaiveBean import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig import io.nekohasekai.sagernet.fmt.naive.toUri @@ -24,13 +26,15 @@ import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig import io.nekohasekai.sagernet.fmt.trojan_go.toUri import io.nekohasekai.sagernet.fmt.tuic.TuicBean -import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig +import io.nekohasekai.sagernet.fmt.tuic.toUri import io.nekohasekai.sagernet.fmt.v2ray.* import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean import io.nekohasekai.sagernet.ktx.app -import io.nekohasekai.sagernet.ktx.applyDefaultValues import io.nekohasekai.sagernet.ui.profile.* -import moe.matsuri.nb4a.Protocols +import moe.matsuri.nb4a.SingBoxOptions.MultiplexOptions +import moe.matsuri.nb4a.proxy.anytls.AnyTLSBean +import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity +import moe.matsuri.nb4a.proxy.anytls.toUri import moe.matsuri.nb4a.proxy.config.ConfigBean import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity import moe.matsuri.nb4a.proxy.neko.* @@ -56,12 +60,14 @@ data class ProxyEntity( var vmessBean: VMessBean? = null, var trojanBean: TrojanBean? = null, var trojanGoBean: TrojanGoBean? = null, + var mieruBean: MieruBean? = null, var naiveBean: NaiveBean? = null, var hysteriaBean: HysteriaBean? = null, var tuicBean: TuicBean? = null, var sshBean: SSHBean? = null, var wgBean: WireGuardBean? = null, var shadowTLSBean: ShadowTLSBean? = null, + var anyTLSBean: AnyTLSBean? = null, var chainBean: ChainBean? = null, var nekoBean: NekoBean? = null, var configBean: ConfigBean? = null, @@ -74,15 +80,16 @@ data class ProxyEntity( const val TYPE_VMESS = 4 const val TYPE_TROJAN = 6 - const val TYPE_TROJAN_GO = 7 - const val TYPE_NAIVE = 9 - const val TYPE_HYSTERIA = 15 - const val TYPE_TUIC = 20 - const val TYPE_SSH = 17 const val TYPE_WG = 18 + const val TYPE_TROJAN_GO = 7 + const val TYPE_NAIVE = 9 + const val TYPE_HYSTERIA = 15 const val TYPE_SHADOWTLS = 19 + const val TYPE_TUIC = 20 + const val TYPE_MIERU = 21 + const val TYPE_ANYTLS = 22 const val TYPE_CONFIG = 998 const val TYPE_NEKO = 999 @@ -91,10 +98,8 @@ data class ProxyEntity( val chainName by lazy { app.getString(R.string.proxy_chain) } - private val placeHolderBean = SOCKSBean().applyDefaultValues() - @JvmField - val CREATOR = object : Serializable.CREATOR() { + val CREATOR = object : CREATOR() { override fun newInstance(): ProxyEntity { return ProxyEntity() @@ -161,31 +166,35 @@ data class ProxyEntity( TYPE_VMESS -> vmessBean = KryoConverters.vmessDeserialize(byteArray) TYPE_TROJAN -> trojanBean = KryoConverters.trojanDeserialize(byteArray) TYPE_TROJAN_GO -> trojanGoBean = KryoConverters.trojanGoDeserialize(byteArray) + TYPE_MIERU -> mieruBean = KryoConverters.mieruDeserialize(byteArray) TYPE_NAIVE -> naiveBean = KryoConverters.naiveDeserialize(byteArray) TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray) TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray) TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray) TYPE_TUIC -> tuicBean = KryoConverters.tuicDeserialize(byteArray) TYPE_SHADOWTLS -> shadowTLSBean = KryoConverters.shadowTLSDeserialize(byteArray) + TYPE_ANYTLS -> anyTLSBean = KryoConverters.anyTLSDeserialize(byteArray) TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray) TYPE_NEKO -> nekoBean = KryoConverters.nekoDeserialize(byteArray) TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray) } } - fun displayType() = when (type) { + fun displayType(): String = when (type) { TYPE_SOCKS -> socksBean!!.protocolName() TYPE_HTTP -> if (httpBean!!.isTLS()) "HTTPS" else "HTTP" TYPE_SS -> "Shadowsocks" TYPE_VMESS -> if (vmessBean!!.isVLESS) "VLESS" else "VMess" TYPE_TROJAN -> "Trojan" TYPE_TROJAN_GO -> "Trojan-Go" + TYPE_MIERU -> "Mieru" TYPE_NAIVE -> "Naïve" - TYPE_HYSTERIA -> "Hysteria" + TYPE_HYSTERIA -> "Hysteria" + hysteriaBean!!.protocolVersion TYPE_SSH -> "SSH" TYPE_WG -> "WireGuard" TYPE_TUIC -> "TUIC" TYPE_SHADOWTLS -> "ShadowTLS" + TYPE_ANYTLS -> "AnyTLS" TYPE_CHAIN -> chainName TYPE_NEKO -> nekoBean!!.displayType() TYPE_CONFIG -> configBean!!.displayType() @@ -203,12 +212,14 @@ data class ProxyEntity( TYPE_VMESS -> vmessBean TYPE_TROJAN -> trojanBean TYPE_TROJAN_GO -> trojanGoBean + TYPE_MIERU -> mieruBean TYPE_NAIVE -> naiveBean TYPE_HYSTERIA -> hysteriaBean TYPE_SSH -> sshBean TYPE_WG -> wgBean TYPE_TUIC -> tuicBean TYPE_SHADOWTLS -> shadowTLSBean + TYPE_ANYTLS -> anyTLSBean TYPE_CHAIN -> chainBean TYPE_NEKO -> nekoBean TYPE_CONFIG -> configBean @@ -225,11 +236,10 @@ data class ProxyEntity( fun haveStandardLink(): Boolean { return when (requireBean()) { - is TuicBean -> false is SSHBean -> false is WireGuardBean -> false is ShadowTLSBean -> false - is NekoBean -> nekoBean!!.haveStandardLink() + is NekoBean -> false is ConfigBean -> false else -> true } @@ -245,7 +255,9 @@ data class ProxyEntity( is TrojanGoBean -> toUri() is NaiveBean -> toUri() is HysteriaBean -> toUri() - is NekoBean -> shareLink() + is TuicBean -> toUri() + is AnyTLSBean -> toUri() + is NekoBean -> "" else -> toUniversalLink() } } @@ -269,17 +281,20 @@ data class ProxyEntity( append("\n\n") append(bean.buildTrojanGoConfig(port)) } + + is MieruBean -> { + append("\n\n") + append(bean.buildMieruConfig(port)) + } + is NaiveBean -> { append("\n\n") append(bean.buildNaiveConfig(port)) } + is HysteriaBean -> { append("\n\n") - append(bean.buildHysteriaConfig(port, null)) - } - is TuicBean -> { - append("\n\n") - append(bean.buildTuicConfig(port, null)) + append(bean.buildHysteria1Config(port, null)) } } } @@ -291,28 +306,39 @@ data class ProxyEntity( fun needExternal(): Boolean { return when (type) { TYPE_TROJAN_GO -> true + TYPE_MIERU -> true TYPE_NAIVE -> true TYPE_HYSTERIA -> !hysteriaBean!!.canUseSingBox() - TYPE_TUIC -> true TYPE_NEKO -> true else -> false } } - fun isV2RayNetworkTcp(): Boolean { - val bean = requireBean() as StandardV2RayBean - return when (bean.type) { - "tcp", "ws", "http" -> true - else -> false - } - } - - fun needCoreMux(): Boolean { + fun singMux(): MultiplexOptions? { return when (type) { - TYPE_VMESS -> isV2RayNetworkTcp() && Protocols.shouldEnableMux("vmess") && !vmessBean!!.isVLESS - TYPE_TROJAN -> isV2RayNetworkTcp() && Protocols.shouldEnableMux("trojan") - TYPE_SS -> !ssBean!!.sUoT && Protocols.shouldEnableMux("shadowsocks") - else -> false + TYPE_VMESS -> MultiplexOptions().apply { + enabled = vmessBean!!.enableMux + padding = vmessBean!!.muxPadding + max_streams = vmessBean!!.muxConcurrency + protocol = when (vmessBean!!.muxType) { + 1 -> "smux" + 2 -> "yamux" + else -> "h2mux" + } + } + + TYPE_TROJAN -> MultiplexOptions().apply { + enabled = trojanBean!!.enableMux + padding = trojanBean!!.muxPadding + max_streams = trojanBean!!.muxConcurrency + protocol = when (trojanBean!!.muxType) { + 1 -> "smux" + 2 -> "yamux" + else -> "h2mux" + } + } + + else -> null } } @@ -323,12 +349,14 @@ data class ProxyEntity( vmessBean = null trojanBean = null trojanGoBean = null + mieruBean = null naiveBean = null hysteriaBean = null sshBean = null wgBean = null tuicBean = null shadowTLSBean = null + anyTLSBean = null chainBean = null configBean = null nekoBean = null @@ -338,62 +366,87 @@ data class ProxyEntity( type = TYPE_SOCKS socksBean = bean } + is HttpBean -> { type = TYPE_HTTP httpBean = bean } + is ShadowsocksBean -> { type = TYPE_SS ssBean = bean } + is VMessBean -> { type = TYPE_VMESS vmessBean = bean } + is TrojanBean -> { type = TYPE_TROJAN trojanBean = bean } + is TrojanGoBean -> { type = TYPE_TROJAN_GO trojanGoBean = bean } + + is MieruBean -> { + type = TYPE_MIERU + mieruBean = bean + } + is NaiveBean -> { type = TYPE_NAIVE naiveBean = bean } + is HysteriaBean -> { type = TYPE_HYSTERIA hysteriaBean = bean } + is SSHBean -> { type = TYPE_SSH sshBean = bean } + is WireGuardBean -> { type = TYPE_WG wgBean = bean } + is TuicBean -> { type = TYPE_TUIC tuicBean = bean } + is ShadowTLSBean -> { type = TYPE_SHADOWTLS shadowTLSBean = bean } + + is AnyTLSBean -> { + type = TYPE_ANYTLS + anyTLSBean = bean + } + is ChainBean -> { type = TYPE_CHAIN chainBean = bean } + is NekoBean -> { type = TYPE_NEKO nekoBean = bean } + is ConfigBean -> { type = TYPE_CONFIG configBean = bean } + else -> error("Undefined type $type") } return this @@ -408,14 +461,15 @@ data class ProxyEntity( TYPE_VMESS -> VMessSettingsActivity::class.java TYPE_TROJAN -> TrojanSettingsActivity::class.java TYPE_TROJAN_GO -> TrojanGoSettingsActivity::class.java + TYPE_MIERU -> MieruSettingsActivity::class.java TYPE_NAIVE -> NaiveSettingsActivity::class.java TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java TYPE_SSH -> SSHSettingsActivity::class.java TYPE_WG -> WireGuardSettingsActivity::class.java TYPE_TUIC -> TuicSettingsActivity::class.java TYPE_SHADOWTLS -> ShadowTLSSettingsActivity::class.java + TYPE_ANYTLS -> AnyTLSSettingsActivity::class.java TYPE_CHAIN -> ChainSettingsActivity::class.java - TYPE_NEKO -> NekoSettingActivity::class.java TYPE_CONFIG -> ConfigSettingActivity::class.java else -> throw IllegalArgumentException() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt index 20cbbc05d..7d610c5e4 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt @@ -8,9 +8,12 @@ import kotlinx.parcelize.Parcelize @Entity(tableName = "rules") @Parcelize +@TypeConverters(StringCollectionConverter::class) data class RuleEntity( @PrimaryKey(autoGenerate = true) var id: Long = 0L, var name: String = "", + @ColumnInfo(defaultValue = "") + var config: String = "", var userOrder: Long = 0L, var enabled: Boolean = false, var domains: String = "", @@ -21,7 +24,7 @@ data class RuleEntity( var source: String = "", var protocol: String = "", var outbound: Long = 0, - var packages: List = listOf(), + var packages: Set = emptySet(), ) : Parcelable { fun displayName(): String { @@ -30,11 +33,12 @@ data class RuleEntity( fun mkSummary(): String { var summary = "" + if (config.isNotBlank()) summary += "[config]\n" if (domains.isNotBlank()) summary += "$domains\n" if (ip.isNotBlank()) summary += "$ip\n" - if (source.isNotBlank()) summary += "source: $source\n" - if (sourcePort.isNotBlank()) summary += "sourcePort: $sourcePort\n" - if (port.isNotBlank()) summary += "port: $port\n" + if (source.isNotBlank()) summary += "src ip: $source\n" + if (sourcePort.isNotBlank()) summary += "src port: $sourcePort\n" + if (port.isNotBlank()) summary += "dst port: $port\n" if (network.isNotBlank()) summary += "network: $network\n" if (protocol.isNotBlank()) summary += "protocol: $protocol\n" if (packages.isNotEmpty()) summary += app.getString( @@ -54,7 +58,7 @@ data class RuleEntity( -1L -> app.getString(R.string.route_bypass) -2L -> app.getString(R.string.route_block) else -> ProfileManager.getProfile(outbound)?.displayName() - ?: app.getString(R.string.route_proxy) + ?: app.getString(R.string.error_title) } } 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 4d57ed3e4..ececc106e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt @@ -1,5 +1,6 @@ package io.nekohasekai.sagernet.database +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @@ -15,7 +16,12 @@ import kotlinx.coroutines.launch @Database( entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class], - version = 2 + version = 6, + autoMigrations = [ + AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6) + ] ) @TypeConverters(value = [KryoConverters::class, GsonConverters::class]) @GenerateRoomMigrations @@ -27,7 +33,8 @@ abstract class SagerDatabase : RoomDatabase() { val instance by lazy { SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs() Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE) - .addMigrations(*SagerDatabase_Migrations.build()) +// .addMigrations(*SagerDatabase_Migrations.build()) + .setJournalMode(JournalMode.TRUNCATE) .allowMainThreadQueries() .enableMultiInstanceInvalidation() .fallbackToDestructiveMigration() diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/StringCollectionConverter.kt b/app/src/main/java/io/nekohasekai/sagernet/database/StringCollectionConverter.kt new file mode 100644 index 000000000..ea45467d8 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/database/StringCollectionConverter.kt @@ -0,0 +1,44 @@ +package io.nekohasekai.sagernet.database + +import androidx.room.TypeConverter + +class StringCollectionConverter { + companion object { + const val SPLIT_FLAG = "," + + /* + @TypeConverter + @JvmStatic + fun fromList(list: List): String = if (list.isEmpty()) { + "" + } else { + list.joinToString(SPLIT_FLAG) + } + + @TypeConverter + @JvmStatic + fun toList(str: String): List = if (str.isBlank()) { + emptyList() + } else { + str.split(SPLIT_FLAG) + } + */ + + + @TypeConverter + @JvmStatic + fun fromSet(set: Set): String = if (set.isEmpty()) { + "" + } else { + set.joinToString(SPLIT_FLAG) + } + + @TypeConverter + @JvmStatic + fun toSet(str: String): Set = if (str.isBlank()) { + emptySet() + } else { + str.split(",").toSet() + } + } +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt index e9296de55..d4ebf9800 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt @@ -16,6 +16,7 @@ abstract class PublicDatabase : RoomDatabase() { val instance by lazy { SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs() Room.databaseBuilder(SagerNet.application, PublicDatabase::class.java, Key.DB_PUBLIC) + .setJournalMode(JournalMode.TRUNCATE) .allowMainThreadQueries() .enableMultiInstanceInvalidation() .fallbackToDestructiveMigration() diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index 9e2635f63..8fe0a2944 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -1,7 +1,7 @@ package io.nekohasekai.sagernet.fmt -import io.nekohasekai.sagernet.IPv6Mode -import io.nekohasekai.sagernet.Key +import android.widget.Toast +import io.nekohasekai.sagernet.* import io.nekohasekai.sagernet.bg.VpnService import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.ProxyEntity @@ -10,7 +10,6 @@ import io.nekohasekai.sagernet.database.SagerDatabase import io.nekohasekai.sagernet.fmt.ConfigBuildResult.IndexEntity import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean import io.nekohasekai.sagernet.fmt.hysteria.buildSingBoxOutboundHysteriaBean -import io.nekohasekai.sagernet.fmt.hysteria.isMultiPort import io.nekohasekai.sagernet.fmt.internal.ChainBean import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean import io.nekohasekai.sagernet.fmt.shadowsocks.buildSingBoxOutboundShadowsocksBean @@ -19,6 +18,7 @@ import io.nekohasekai.sagernet.fmt.socks.buildSingBoxOutboundSocksBean import io.nekohasekai.sagernet.fmt.ssh.SSHBean import io.nekohasekai.sagernet.fmt.ssh.buildSingBoxOutboundSSHBean import io.nekohasekai.sagernet.fmt.tuic.TuicBean +import io.nekohasekai.sagernet.fmt.tuic.buildSingBoxOutboundTuicBean import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean import io.nekohasekai.sagernet.fmt.v2ray.buildSingBoxOutboundStandardV2RayBean import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean @@ -26,30 +26,27 @@ import io.nekohasekai.sagernet.fmt.wireguard.buildSingBoxOutboundWireguardBean import io.nekohasekai.sagernet.ktx.isIpAddress import io.nekohasekai.sagernet.ktx.mkPort import io.nekohasekai.sagernet.utils.PackageCache +import moe.matsuri.nb4a.* import moe.matsuri.nb4a.SingBoxOptions.* -import moe.matsuri.nb4a.applyDNSNetworkSettings -import moe.matsuri.nb4a.checkEmpty -import moe.matsuri.nb4a.makeSingBoxRule import moe.matsuri.nb4a.plugin.Plugins +import moe.matsuri.nb4a.proxy.anytls.AnyTLSBean +import moe.matsuri.nb4a.proxy.anytls.buildSingBoxOutboundAnyTLSBean import moe.matsuri.nb4a.proxy.config.ConfigBean import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean import moe.matsuri.nb4a.proxy.shadowtls.buildSingBoxOutboundShadowTLSBean import moe.matsuri.nb4a.utils.JavaUtil.gson +import moe.matsuri.nb4a.utils.Util +import moe.matsuri.nb4a.utils.listByLineOrComma import okhttp3.HttpUrl.Companion.toHttpUrlOrNull const val TAG_MIXED = "mixed-in" -const val TAG_TRANS = "trans-in" const val TAG_PROXY = "proxy" const val TAG_DIRECT = "direct" const val TAG_BYPASS = "bypass" const val TAG_BLOCK = "block" -const val TAG_DNS_IN = "dns-in" -const val TAG_DNS_OUT = "dns-out" - const val LOCALHOST = "127.0.0.1" -const val LOCAL_DNS_SERVER = "underlying://0.0.0.0" class ConfigBuildResult( var config: String, @@ -57,26 +54,11 @@ class ConfigBuildResult( var mainEntId: Long, var trafficMap: Map>, var profileTagMap: Map, - val alerts: List>, val selectorGroupId: Long, ) { data class IndexEntity(var chain: LinkedHashMap) } -fun mergeJSON(j: String, to: MutableMap) { - if (j.isBlank()) return - val m = gson.fromJson(j, to.javaClass) - m.forEach { (k, v) -> - if (v is Map<*, *> && to[k] is Map<*, *>) { - val currentMap = (to[k] as Map<*, *>).toMutableMap() - currentMap += v - to[k] = currentMap - } else { - to[k] = v - } - } -} - fun buildConfig( proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean = false ): ConfigBuildResult { @@ -90,7 +72,6 @@ fun buildConfig( proxy.id, // mapOf(TAG_PROXY to listOf(proxy)), // mapOf(proxy.id to TAG_PROXY), // - listOf(), -1L ) } @@ -101,7 +82,6 @@ fun buildConfig( val globalOutbounds = HashMap() val selectorNames = ArrayList() val group = SagerDatabase.groupDao.getById(proxy.groupId) - val optionsToMerge = proxy.requireBean().customConfigJson ?: "" fun ProxyEntity.resolveChainInternal(): MutableList { val bean = requireBean() @@ -130,8 +110,9 @@ fun buildConfig( } fun ProxyEntity.resolveChain(): MutableList { - val frontProxy = group?.frontProxy?.let { SagerDatabase.proxyDao.getById(it) } - val landingProxy = group?.landingProxy?.let { SagerDatabase.proxyDao.getById(it) } + val thisGroup = SagerDatabase.groupDao.getById(groupId) + val frontProxy = thisGroup?.frontProxy?.let { SagerDatabase.proxyDao.getById(it) } + val landingProxy = thisGroup?.landingProxy?.let { SagerDatabase.proxyDao.getById(it) } val list = resolveChainInternal() if (frontProxy != null) { list.add(frontProxy) @@ -148,31 +129,25 @@ fun buildConfig( rule.outbound.takeIf { it > 0 && it != proxy.id } }.toHashSet().toList()).associateBy { it.id } val buildSelector = !forTest && group?.isSelector == true && !forExport - val uidListDNSRemote = mutableListOf() - val uidListDNSDirect = mutableListOf() - val domainListDNSRemote = mutableListOf() - val domainListDNSDirect = mutableListOf() + val userDNSRuleList = mutableListOf() val domainListDNSDirectForce = mutableListOf() - val domainListDNSBlock = mutableListOf() val bypassDNSBeans = hashSetOf() val isVPN = DataStore.serviceMode == Key.MODE_VPN val bind = if (!forTest && DataStore.allowAccess) "0.0.0.0" else LOCALHOST val remoteDns = DataStore.remoteDns.split("\n") .mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith("#") } } - var directDNS = DataStore.directDns.split("\n") + val directDNS = DataStore.directDns.split("\n") .mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith("#") } } val enableDnsRouting = DataStore.enableDnsRouting - val useFakeDns = DataStore.enableFakeDns && !forTest && DataStore.ipv6Mode != IPv6Mode.ONLY - val needSniff = DataStore.trafficSniffing + val useFakeDns = DataStore.enableFakeDns && !forTest + val needSniff = DataStore.trafficSniffing > 0 + val needSniffOverride = DataStore.trafficSniffing == 2 val externalIndexMap = ArrayList() - val requireTransproxy = if (forTest) false else DataStore.requireTransproxy val ipv6Mode = if (forTest) IPv6Mode.ENABLE else DataStore.ipv6Mode - val resolveDestination = DataStore.resolveDestination - val alerts = mutableListOf>() fun genDomainStrategy(noAsIs: Boolean): String { return when { - !resolveDestination && !noAsIs -> "" + !noAsIs -> "" ipv6Mode == IPv6Mode.DISABLE -> "ipv4_only" ipv6Mode == IPv6Mode.PREFER -> "prefer_ipv6" ipv6Mode == IPv6Mode.ONLY -> "ipv6_only" @@ -185,7 +160,6 @@ fun buildConfig( clash_api = ClashAPIOptions().apply { external_controller = "127.0.0.1:9090" external_ui = "../files/yacd" - cache_file = "../cache/clash.db" } } @@ -201,22 +175,21 @@ fun buildConfig( } dns = DNSOptions().apply { - // TODO nb4a hosts? -// hosts = DataStore.hosts.split("\n") -// .filter { it.isNotBlank() } -// .associate { it.substringBefore(" ") to it.substringAfter(" ") } -// .toMutableMap() - servers = mutableListOf() rules = mutableListOf() + independent_cache = true + } - when (ipv6Mode) { - IPv6Mode.DISABLE -> { - strategy = "ipv4_only" - } - IPv6Mode.ONLY -> { - strategy = "ipv6_only" - } + fun autoDnsDomainStrategy(s: String): String? { + if (s.isNotEmpty()) { + return s + } + return when (ipv6Mode) { + IPv6Mode.DISABLE -> "ipv4_only" + IPv6Mode.ENABLE -> "prefer_ipv4" + IPv6Mode.PREFER -> "prefer_ipv6" + IPv6Mode.ONLY -> "ipv6_only" + else -> null } } @@ -226,17 +199,25 @@ fun buildConfig( if (isVPN) inbounds.add(Inbound_TunOptions().apply { type = "tun" tag = "tun-in" - stack = if (DataStore.tunImplementation == 1) "system" else "gvisor" - sniff = needSniff + stack = when (DataStore.tunImplementation) { + TunImplementation.GVISOR -> "gvisor" + TunImplementation.SYSTEM -> "system" + else -> "mixed" + } endpoint_independent_nat = true - domain_strategy = genDomainStrategy(false) + mtu = DataStore.mtu + domain_strategy = genDomainStrategy(DataStore.resolveDestination) + sniff = needSniff + sniff_override_destination = needSniffOverride when (ipv6Mode) { IPv6Mode.DISABLE -> { inet4_address = listOf(VpnService.PRIVATE_VLAN4_CLIENT + "/28") } + IPv6Mode.ONLY -> { inet6_address = listOf(VpnService.PRIVATE_VLAN6_CLIENT + "/126") } + else -> { inet4_address = listOf(VpnService.PRIVATE_VLAN4_CLIENT + "/28") inet6_address = listOf(VpnService.PRIVATE_VLAN6_CLIENT + "/126") @@ -248,48 +229,19 @@ fun buildConfig( tag = TAG_MIXED listen = bind listen_port = DataStore.mixedPort - domain_strategy = genDomainStrategy(false) - if (needSniff) { - sniff = true -// destOverride = when { -// useFakeDns && !trafficSniffing -> listOf("fakedns") -// useFakeDns -> listOf("fakedns", "http", "tls", "quic") -// else -> listOf("http", "tls", "quic") -// } -// metadataOnly = useFakeDns && !trafficSniffing -// routeOnly = true - } + domain_strategy = genDomainStrategy(DataStore.resolveDestination) + sniff = needSniff + sniff_override_destination = needSniffOverride }) } - if (requireTransproxy) { - if (DataStore.transproxyMode == 1) { - inbounds.add(Inbound_TProxyOptions().apply { - type = "tproxy" - tag = TAG_TRANS - listen = bind - listen_port = DataStore.transproxyPort - sniff = needSniff - domain_strategy = genDomainStrategy(false) - }) - } else { - inbounds.add(Inbound_RedirectOptions().apply { - type = "redirect" - tag = TAG_TRANS - listen = bind - listen_port = DataStore.transproxyPort - sniff = needSniff - domain_strategy = genDomainStrategy(false) - }) - } - } - outbounds = mutableListOf() // init routing object route = RouteOptions().apply { auto_detect_interface = true rules = mutableListOf() + rule_set = mutableListOf() } // returns outbound tag @@ -302,20 +254,20 @@ fun buildConfig( add(entity) } - var currentOutbound = mutableMapOf() - lateinit var pastOutbound: MutableMap + var currentOutbound: SingBoxOption + lateinit var pastOutbound: SingBoxOption lateinit var pastInboundTag: String var pastEntity: ProxyEntity? = null val externalChainMap = LinkedHashMap() externalIndexMap.add(IndexEntity(externalChainMap)) - val chainOutbounds = ArrayList>() + val chainOutbounds = ArrayList() // chainTagOut: v2ray outbound tag for this chain var chainTagOut = "" val chainTag = "c-$chainId" var muxApplied = false - var currentDomainStrategy = genDomainStrategy(false) + val defaultServerDomainStrategy = SingBoxOptionsUtil.domainStrategy("server") profileList.forEachIndexed { index, proxyEntity -> val bean = proxyEntity.requireBean() @@ -336,13 +288,6 @@ fun buildConfig( bypassDNSBeans += proxyEntity.requireBean() } - if (needGlobal) { - globalOutbounds[proxyEntity.id]?.let { - if (index == 0) chainTagOut = it // single, duplicate chain - return@forEachIndexed - } - } - // last profile set as "proxy" if (chainId == 0L && index == 0) { tagOut = TAG_PROXY @@ -353,10 +298,6 @@ fun buildConfig( tagOut = selectorName(bean.displayName()) } - // now tagOut is determined - if (needGlobal) { - globalOutbounds[proxyEntity.id] = tagOut - } // chain rules if (index > 0) { @@ -367,93 +308,118 @@ fun buildConfig( outbound = tagOut }) } else { - pastOutbound["detour"] = tagOut + pastOutbound._hack_config_map["detour"] = tagOut } } else { // index == 0 means last profile in chain / not chain chainTagOut = tagOut } - // Chain outbound - if (proxyEntity.needExternal()) { + // now tagOut is determined + if (needGlobal) { + globalOutbounds[proxyEntity.id]?.let { + if (index == 0) chainTagOut = it // single, duplicate chain + return@forEachIndexed + } + globalOutbounds[proxyEntity.id] = tagOut + } + + if (proxyEntity.needExternal()) { // externel outbound val localPort = mkPort() externalChainMap[localPort] = proxyEntity currentOutbound = Outbound_SocksOptions().apply { type = "socks" server = LOCALHOST server_port = localPort - }.asMap() + } } else { // internal outbound currentOutbound = when (bean) { - is ConfigBean -> - gson.fromJson(bean.config, currentOutbound.javaClass) + is ConfigBean -> CustomSingBoxOption(bean.config) + is ShadowTLSBean -> // before StandardV2RayBean - buildSingBoxOutboundShadowTLSBean(bean).asMap() + buildSingBoxOutboundShadowTLSBean(bean) + is StandardV2RayBean -> // http/trojan/vmess/vless - buildSingBoxOutboundStandardV2RayBean(bean).asMap() + buildSingBoxOutboundStandardV2RayBean(bean) + is HysteriaBean -> - buildSingBoxOutboundHysteriaBean(bean).asMap() + buildSingBoxOutboundHysteriaBean(bean) + + is TuicBean -> + buildSingBoxOutboundTuicBean(bean) + is SOCKSBean -> - buildSingBoxOutboundSocksBean(bean).asMap() + buildSingBoxOutboundSocksBean(bean) + is ShadowsocksBean -> - buildSingBoxOutboundShadowsocksBean(bean).asMap() + buildSingBoxOutboundShadowsocksBean(bean) + is WireGuardBean -> - buildSingBoxOutboundWireguardBean(bean).asMap() + buildSingBoxOutboundWireguardBean(bean) + is SSHBean -> - buildSingBoxOutboundSSHBean(bean).asMap() + buildSingBoxOutboundSSHBean(bean) + + is AnyTLSBean -> + buildSingBoxOutboundAnyTLSBean(bean) + else -> throw IllegalStateException("can't reach") } - currentOutbound.apply { - // TODO nb4a keepAliveInterval? -// val keepAliveInterval = DataStore.tcpKeepAliveInterval -// val needKeepAliveInterval = keepAliveInterval !in intArrayOf(0, 15) - - if (!muxApplied && proxyEntity.needCoreMux()) { + // internal mux + if (!muxApplied) { + val muxObj = proxyEntity.singMux() + if (muxObj != null && muxObj.enabled) { muxApplied = true - currentOutbound["multiplex"] = MultiplexOptions().apply { - enabled = true - max_streams = DataStore.muxConcurrency - } + currentOutbound._hack_config_map["multiplex"] = muxObj.asMap() } } + } - // custom JSON merge - if (bean.customOutboundJson.isNotBlank()) { - mergeJSON(bean.customOutboundJson, currentOutbound) + // internal & external + currentOutbound.apply { + // udp over tcp + try { + val sUoT = bean.javaClass.getField("sUoT").get(bean) + if (sUoT is Boolean && sUoT) { + _hack_config_map["udp_over_tcp"] = true + } + } catch (_: Exception) { } - } - pastEntity?.requireBean()?.apply { - // don't loopback - if (currentDomainStrategy != "" && !serverAddress.isIpAddress()) { - domainListDNSDirectForce.add("full:$serverAddress") + // domain_strategy + pastEntity?.requireBean()?.apply { + // don't loopback + if (defaultServerDomainStrategy != "" && !serverAddress.isIpAddress()) { + domainListDNSDirectForce.add("full:$serverAddress") + } } - } - if (forTest) { - currentDomainStrategy = "" - } + _hack_config_map["domain_strategy"] = + if (forTest) "" else defaultServerDomainStrategy - currentOutbound["tag"] = tagOut - currentOutbound["domain_strategy"] = currentDomainStrategy + _hack_config_map["tag"] = tagOut + + _hack_custom_config = bean.customOutboundJson + } // External proxy need a dokodemo-door inbound to forward the traffic // For external proxy software, their traffic must goes to v2ray-core to use protected fd. + bean.finalAddress = bean.serverAddress + bean.finalPort = bean.serverPort if (bean.canMapping() && proxyEntity.needExternal()) { // With ss protect, don't use mapping var needExternal = true if (index == profileList.lastIndex) { val pluginId = when (bean) { - is HysteriaBean -> "hysteria-plugin" - is TuicBean -> "tuic-plugin" + is HysteriaBean -> if (bean.protocolVersion == 1) "hysteria-plugin" else "hysteria2-plugin" else -> "" } if (Plugins.isUsingMatsuriExe(pluginId)) { needExternal = false - } else if (bean is HysteriaBean) { - throw Exception("not supported hysteria-plugin (SagerNet)") + } else if (Plugins.getPluginExternal(pluginId) != null) { + throw Exception("You are using an unsupported $pluginId, please download the correct plugin.") } } if (needExternal) { @@ -495,8 +461,8 @@ fun buildConfig( // build outbounds if (buildSelector) { - val list = group?.id?.let { SagerDatabase.proxyDao.getByGroup(it) } - list?.forEach { + val list = group.id.let { SagerDatabase.proxyDao.getByGroup(it) } + list.forEach { tagMap[it.id] = buildChain(it.id, it) } outbounds.add(0, Outbound_SelectorOptions().apply { @@ -504,7 +470,7 @@ fun buildConfig( tag = TAG_PROXY default_ = tagMap[proxy.id] outbounds = tagMap.values.toList() - }.asMap()) + }) } else { buildChain(0, proxy) } @@ -518,30 +484,38 @@ fun buildConfig( if (rule.packages.isNotEmpty()) { PackageCache.awaitLoadSync() } - val uidList2 = rule.packages.map { + val uidList = rule.packages.map { if (!isVPN) { - alerts.add(0 to rule.displayName()) + Toast.makeText( + SagerNet.application, + SagerNet.application.getString(R.string.route_need_vpn, rule.displayName()), + Toast.LENGTH_SHORT + ).show() } PackageCache[it]?.takeIf { uid -> uid >= 1000 } }.toHashSet().filterNotNull() + val ruleSets = mutableListOf() val ruleObj = Rule_DefaultOptions().apply { - if (uidList2.isNotEmpty()) { + if (uidList.isNotEmpty()) { PackageCache.awaitLoadSync() - user_id = uidList2 + user_id = uidList } - var domainList2: List? = null + var domainList: List? = null if (rule.domains.isNotBlank()) { - domainList2 = rule.domains.split("\n") - makeSingBoxRule(domainList2, false) + domainList = rule.domains.listByLineOrComma() + makeSingBoxRule(domainList, false) } if (rule.ip.isNotBlank()) { - makeSingBoxRule(rule.ip.split("\n"), true) + makeSingBoxRule(rule.ip.listByLineOrComma(), true) } + + if (rule_set != null) generateRuleSet(rule_set, ruleSets) + if (rule.port.isNotBlank()) { port = mutableListOf() port_range = mutableListOf() - rule.port.split(",").map { + rule.port.listByLineOrComma().map { if (it.contains(":")) { port_range.add(it) } else { @@ -552,7 +526,7 @@ fun buildConfig( if (rule.sourcePort.isNotBlank()) { source_port = mutableListOf() source_port_range = mutableListOf() - rule.sourcePort.split(",").map { + rule.sourcePort.listByLineOrComma().map { if (it.contains(":")) { source_port_range.add(it) } else { @@ -561,78 +535,88 @@ fun buildConfig( } } if (rule.network.isNotBlank()) { - network = rule.network + network = listOf(rule.network) } if (rule.source.isNotBlank()) { - source_ip_cidr = rule.source.split("\n") + source_ip_cidr = rule.source.listByLineOrComma() } if (rule.protocol.isNotBlank()) { - protocol = rule.protocol.split("\n") + protocol = rule.protocol.listByLineOrComma() } - // also bypass lookup - // cannot use other outbound profile to lookup... - if (rule.outbound == -1L) { - uidListDNSDirect += uidList2 - if (domainList2 != null) domainListDNSDirect += domainList2 - } else if (rule.outbound == 0L) { - uidListDNSRemote += uidList2 - if (domainList2 != null) domainListDNSRemote += domainList2 - } else if (rule.outbound == -2L) { - if (domainList2 != null) domainListDNSBlock += domainList2 + fun makeDnsRuleObj(): DNSRule_DefaultOptions { + return DNSRule_DefaultOptions().apply { + if (uidList.isNotEmpty()) user_id = uidList + domainList?.let { makeSingBoxRule(it) } + } + } + + when (rule.outbound) { + -1L -> { + userDNSRuleList += makeDnsRuleObj().apply { server = "dns-direct" } + } + + 0L -> { + if (useFakeDns) userDNSRuleList += makeDnsRuleObj().apply { + server = "dns-fake" + inbound = listOf("tun-in") + } + userDNSRuleList += makeDnsRuleObj().apply { + server = "dns-remote" + } + } + + -2L -> { + userDNSRuleList += makeDnsRuleObj().apply { + server = "dns-block" + disable_cache = true + } + } } outbound = when (val outId = rule.outbound) { 0L -> TAG_PROXY -1L -> TAG_BYPASS -2L -> TAG_BLOCK - else -> if (outId == proxy.id) TAG_PROXY else tagMap[outId] - ?: throw Exception("invalid rule") + else -> if (outId == proxy.id) TAG_PROXY else tagMap[outId] ?: "" } + + _hack_custom_config = rule.config } if (!ruleObj.checkEmpty()) { - route.rules.add(ruleObj) + if (ruleObj.outbound.isNullOrBlank()) { + Toast.makeText( + SagerNet.application, + "Warning: " + rule.displayName() + ": A non-existent outbound was specified.", + Toast.LENGTH_LONG + ).show() + } else { + // block 改用新的写法 + if (ruleObj.outbound == TAG_BLOCK) { + ruleObj.outbound = null + ruleObj.action = "reject" + } + route.rules.add(ruleObj) + route.rule_set.addAll(ruleSets) + } } } + // 对 rule_set tag 去重 + if (route.rule_set != null) { + route.rule_set = route.rule_set.distinctBy { it.tag } + } + for (freedom in arrayOf(TAG_DIRECT, TAG_BYPASS)) outbounds.add(Outbound().apply { tag = freedom type = "direct" - }.asMap()) - - outbounds.add(Outbound().apply { - tag = TAG_BLOCK - type = "block" - }.asMap()) - - if (!forTest) { - inbounds.add(0, Inbound_DirectOptions().apply { - type = "direct" - tag = TAG_DNS_IN - listen = bind - listen_port = DataStore.localDNSPort - override_address = "8.8.8.8" - override_port = 53 - }) - - outbounds.add(Outbound().apply { - type = "dns" - tag = TAG_DNS_OUT - }.asMap()) - } - - if (DataStore.directDnsUseSystem) { - // finally able to use "localDns" now... - directDNS = listOf(LOCAL_DNS_SERVER) - } + }) // Bypass Lookup for the first profile bypassDNSBeans.forEach { var serverAddr = it.serverAddress - if (it is HysteriaBean && it.isMultiPort()) { - serverAddr = it.serverAddress.substringBeforeLast(":") - } + if (it is ConfigBean) { var config = mutableMapOf() config = gson.fromJson(it.config, config.javaClass) @@ -658,137 +642,94 @@ fun buildConfig( } } - // remote dns obj - remoteDns.firstOrNull().let { - dns.servers.add(DNSServerOptions().apply { - address = it ?: throw Exception("No remote DNS, check your settings!") - tag = "dns-remote" - address_resolver = "dns-direct" - applyDNSNetworkSettings(false) - }) - } + dns.servers.add(DNSServerOptions().apply { + address = "rcode://success" + tag = "dns-block" + }) + + dns.servers.add(DNSServerOptions().apply { + address = "local" + tag = "dns-local" + detour = TAG_DIRECT + }) - // add directDNS objects here directDNS.firstOrNull().let { dns.servers.add(DNSServerOptions().apply { address = it ?: throw Exception("No direct DNS, check your settings!") tag = "dns-direct" detour = TAG_DIRECT address_resolver = "dns-local" - applyDNSNetworkSettings(true) + strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag)) }) } - dns.servers.add(DNSServerOptions().apply { - address = LOCAL_DNS_SERVER - tag = "dns-local" - detour = TAG_DIRECT - }) - dns.servers.add(DNSServerOptions().apply { - address = "rcode://success" - tag = "dns-block" - }) + + remoteDns.firstOrNull().let { + // Always use direct DNS for urlTest + if (!forTest) dns.servers.add(DNSServerOptions().apply { + address = it ?: throw Exception("No remote DNS, check your settings!") + tag = "dns-remote" + address_resolver = "dns-direct" + strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag)) + }) + } + + dns.final_ = if (forTest) "dns-direct" else "dns-remote" // dns object user rules if (enableDnsRouting) { - val dnsRuleObj = mutableListOf() - if (uidListDNSRemote.isNotEmpty()) { - if (useFakeDns) dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - user_id = uidListDNSRemote.toHashSet().toList() - server = "dns-fake" - inbound = listOf("tun-in") - } - ) - dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - user_id = uidListDNSRemote.toHashSet().toList() - server = "dns-remote" - } - ) - } - if (domainListDNSRemote.isNotEmpty()) { - if (useFakeDns) dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - makeSingBoxRule(domainListDNSRemote.toHashSet().toList()) - server = "dns-fake" - inbound = listOf("tun-in") - } - ) - dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - makeSingBoxRule(domainListDNSRemote.toHashSet().toList()) - server = "dns-remote" - } - ) - } - if (uidListDNSDirect.isNotEmpty()) { - dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - user_id = uidListDNSDirect.toHashSet().toList() - server = "dns-direct" - } - ) - } - if (domainListDNSDirect.isNotEmpty()) { - dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - makeSingBoxRule(domainListDNSDirect.toHashSet().toList()) - server = "dns-direct" - } - ) - } - if (domainListDNSBlock.isNotEmpty()) { - dnsRuleObj.add( - DNSRule_DefaultOptions().apply { - makeSingBoxRule(domainListDNSBlock.toHashSet().toList()) - server = "dns-block" - disable_cache = true - } - ) - } - dnsRuleObj.forEach { + userDNSRuleList.forEach { if (!it.checkEmpty()) dns.rules.add(it) } } if (forTest) { - // Disable DNS for test - dns.servers = listOf( - DNSServerOptions().apply { - address = LOCAL_DNS_SERVER - tag = "dns-local" - detour = TAG_DIRECT - } - ) // Always use system DNS for urlTest dns.rules = listOf() } else { // built-in DNS rules route.rules.add(0, Rule_DefaultOptions().apply { - inbound = listOf(TAG_DNS_IN) - outbound = TAG_DNS_OUT + protocol = listOf("dns") + action = "hijack-dns" }) route.rules.add(0, Rule_DefaultOptions().apply { port = listOf(53) - outbound = TAG_DNS_OUT - }) // TODO new mode use system dns? + action = "hijack-dns" + }) if (DataStore.bypassLanInCore) { route.rules.add(Rule_DefaultOptions().apply { outbound = TAG_BYPASS - geoip = listOf("private") + ip_is_private = true }) } // block mcast route.rules.add(Rule_DefaultOptions().apply { ip_cidr = listOf("224.0.0.0/3", "ff00::/8") source_ip_cidr = listOf("224.0.0.0/3", "ff00::/8") - outbound = TAG_BLOCK + action = "reject" }) - dns.rules.add(DNSRule_DefaultOptions().apply { - domain_suffix = listOf(".arpa.", ".arpa") - server = "dns-block" - disable_cache = true + // FakeDNS obj + if (useFakeDns) { + dns.fakeip = DNSFakeIPOptions().apply { + enabled = true + inet4_range = "198.18.0.0/15" + inet6_range = "fc00::/18" + } + dns.servers.add(DNSServerOptions().apply { + address = "fakeip" + tag = "dns-fake" + strategy = "ipv4_only" + }) + dns.rules.add(DNSRule_DefaultOptions().apply { + inbound = listOf("tun-in") + server = "dns-fake" + disable_cache = true + }) + } + // avoid loopback + dns.rules.add(0, DNSRule_DefaultOptions().apply { + outbound = mutableListOf("any") + server = "dns-direct" }) - // force bypass + // force bypass (always top DNS rule) if (domainListDNSDirectForce.isNotEmpty()) { dns.rules.add(0, DNSRule_DefaultOptions().apply { makeSingBoxRule(domainListDNSDirectForce.toHashSet().toList()) @@ -797,34 +738,17 @@ fun buildConfig( } } - // fakedns obj - if (useFakeDns) { - dns.servers.add(DNSServerOptions().apply { - address = "fakedns://" + VpnService.FAKEDNS_VLAN4_CLIENT + "/15" - tag = "dns-fake" - strategy = "ipv4_only" - }) - dns.rules.add(0, DNSRule_DefaultOptions().apply { - auth_user = listOf("fakedns") - server = "dns-remote" - }) - dns.rules.add(DNSRule_DefaultOptions().apply { - inbound = listOf("tun-in") - server = "dns-fake" - disable_cache = true - }) - } + if (!forTest) _hack_custom_config = DataStore.globalCustomConfig }.let { + val configMap = it.asMap() + Util.mergeJSON(configMap, proxy.requireBean().customConfigJson) ConfigBuildResult( - gson.toJson(it.asMap().apply { - mergeJSON(optionsToMerge, this) - }), + gson.toJson(configMap), externalIndexMap, proxy.id, trafficMap, tagMap, - alerts, - if (buildSelector) group!!.id else -1L + if (buildSelector) group.id else -1L ) } 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 b9a6338da..8fb9951ba 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java @@ -13,8 +13,10 @@ import io.nekohasekai.sagernet.fmt.http.HttpBean; import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean; import io.nekohasekai.sagernet.fmt.internal.ChainBean; +import io.nekohasekai.sagernet.fmt.mieru.MieruBean; import io.nekohasekai.sagernet.fmt.naive.NaiveBean; import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean; +import moe.matsuri.nb4a.proxy.anytls.AnyTLSBean; import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean; import io.nekohasekai.sagernet.fmt.socks.SOCKSBean; import io.nekohasekai.sagernet.fmt.ssh.SSHBean; @@ -99,6 +101,12 @@ public static TrojanGoBean trojanGoDeserialize(byte[] bytes) { return deserialize(new TrojanGoBean(), bytes); } + @TypeConverter + public static MieruBean mieruDeserialize(byte[] bytes) { + if (JavaUtil.isEmpty(bytes)) return null; + return deserialize(new MieruBean(), bytes); + } + @TypeConverter public static NaiveBean naiveDeserialize(byte[] bytes) { if (JavaUtil.isEmpty(bytes)) return null; @@ -135,6 +143,13 @@ public static ShadowTLSBean shadowTLSDeserialize(byte[] bytes) { return deserialize(new ShadowTLSBean(), bytes); } + @TypeConverter + public static AnyTLSBean anyTLSDeserialize(byte[] bytes) { + if (JavaUtil.isEmpty(bytes)) return null; + return deserialize(new AnyTLSBean(), bytes); + } + + @TypeConverter public static ChainBean chainDeserialize(byte[] bytes) { if (JavaUtil.isEmpty(bytes)) return null; diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt index 6cd765856..42769e214 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt @@ -14,35 +14,42 @@ enum class PluginEntry( SagerNet.application.getString(R.string.action_trojan_go), "io.nekohasekai.sagernet.plugin.trojan_go" ), + MieruProxy( + "mieru-plugin", + SagerNet.application.getString(R.string.action_mieru), + "moe.matsuri.exe.mieru", + DownloadSource( + playStore = false, + fdroid = false, + downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=mieru" + ) + ), NaiveProxy( "naive-plugin", SagerNet.application.getString(R.string.action_naive), - "io.nekohasekai.sagernet.plugin.naive" + "moe.matsuri.exe.naive", + DownloadSource( + playStore = false, + fdroid = false, + downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=naive" + ) ), Hysteria( "hysteria-plugin", SagerNet.application.getString(R.string.action_hysteria), - "moe.matsuri.exe.hysteria", DownloadSource( + "moe.matsuri.exe.hysteria", + DownloadSource( playStore = false, fdroid = false, downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=Hysteria" ) ), - TUIC( - "tuic-plugin", - SagerNet.application.getString(R.string.action_tuic), - "moe.matsuri.exe.tuic", DownloadSource( - playStore = false, - fdroid = false, - downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=tuic" - ) - ), ; data class DownloadSource( val playStore: Boolean = true, val fdroid: Boolean = true, - val downloadLink: String = "https://sagernet.org/download/" + val downloadLink: String = "https://matsuridayo.github.io/" ) companion object { 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 c55c58b71..1a91eeff8 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt @@ -10,11 +10,13 @@ object TypeMap : HashMap() { this["vmess"] = ProxyEntity.TYPE_VMESS this["trojan"] = ProxyEntity.TYPE_TROJAN this["trojan-go"] = ProxyEntity.TYPE_TROJAN_GO + this["mieru"] = ProxyEntity.TYPE_MIERU this["naive"] = ProxyEntity.TYPE_NAIVE this["hysteria"] = ProxyEntity.TYPE_HYSTERIA this["ssh"] = ProxyEntity.TYPE_SSH this["wg"] = ProxyEntity.TYPE_WG this["tuic"] = ProxyEntity.TYPE_TUIC + this["anytls"] = ProxyEntity.TYPE_ANYTLS this["neko"] = ProxyEntity.TYPE_NEKO this["config"] = ProxyEntity.TYPE_CONFIG } 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 index 239c9d4ab..6a5f83c03 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java @@ -9,27 +9,21 @@ import io.nekohasekai.sagernet.fmt.AbstractBean; import io.nekohasekai.sagernet.fmt.KryoConverters; +import io.nekohasekai.sagernet.ktx.NetsKt; +import kotlin.text.StringsKt; public class HysteriaBean extends AbstractBean { + public Integer protocolVersion; - 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; + // Use serverPorts instead of serverPort + public String serverPorts; - public static final int PROTOCOL_UDP = 0; - public static final int PROTOCOL_FAKETCP = 1; - public static final int PROTOCOL_WECHAT_VIDEO = 2; - - public Integer protocol; + // HY1 & 2 + public String authPayload; public String obfuscation; public String sni; - public String alpn; public String caText; - public Integer uploadMbps; public Integer downloadMbps; public Boolean allowInsecure; @@ -38,6 +32,20 @@ public class HysteriaBean extends AbstractBean { public Boolean disableMtuDiscovery; public Integer hopInterval; + // HY1 + + public String alpn; + + 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 static final int PROTOCOL_UDP = 0; + public static final int PROTOCOL_FAKETCP = 1; + public static final int PROTOCOL_WECHAT_VIDEO = 2; + public Integer protocol; + @Override public boolean canMapping() { return protocol != PROTOCOL_FAKETCP; @@ -46,6 +54,8 @@ public boolean canMapping() { @Override public void initializeDefaultValues() { super.initializeDefaultValues(); + if (protocolVersion == null) protocolVersion = 2; + if (authPayloadType == null) authPayloadType = TYPE_NONE; if (authPayload == null) authPayload = ""; if (protocol == null) protocol = PROTOCOL_UDP; @@ -53,21 +63,30 @@ public void initializeDefaultValues() { if (sni == null) sni = ""; if (alpn == null) alpn = ""; if (caText == null) caText = ""; - - if (uploadMbps == null) uploadMbps = 10; - if (downloadMbps == null) downloadMbps = 50; if (allowInsecure == null) allowInsecure = false; + if (protocolVersion == 1) { + if (uploadMbps == null) uploadMbps = 10; + if (downloadMbps == null) downloadMbps = 50; + } else { + if (uploadMbps == null) uploadMbps = 0; + if (downloadMbps == null) downloadMbps = 0; + } + if (streamReceiveWindow == null) streamReceiveWindow = 0; if (connectionReceiveWindow == null) connectionReceiveWindow = 0; if (disableMtuDiscovery == null) disableMtuDiscovery = false; if (hopInterval == null) hopInterval = 10; + if (serverPorts == null) serverPorts = "443"; } @Override public void serialize(ByteBufferOutput output) { - output.writeInt(5); + output.writeInt(7); super.serialize(output); + + output.writeInt(protocolVersion); + output.writeInt(authPayloadType); output.writeString(authPayload); output.writeInt(protocol); @@ -84,13 +103,18 @@ public void serialize(ByteBufferOutput output) { output.writeInt(connectionReceiveWindow); output.writeBoolean(disableMtuDiscovery); output.writeInt(hopInterval); - + output.writeString(serverPorts); } @Override public void deserialize(ByteBufferInput input) { int version = input.readInt(); super.deserialize(input); + if (version >= 7) { + protocolVersion = input.readInt(); + } else { + protocolVersion = 1; + } authPayloadType = input.readInt(); authPayload = input.readString(); if (version >= 3) { @@ -113,6 +137,17 @@ public void deserialize(ByteBufferInput input) { if (version >= 5) { hopInterval = input.readInt(); } + if (version >= 6) { + serverPorts = input.readString(); + } else { + // old update to new + if (HysteriaFmtKt.isMultiPort(serverAddress)) { + serverPorts = StringsKt.substringAfterLast(serverAddress, ":", serverAddress); + serverAddress = StringsKt.substringBeforeLast(serverAddress, ":", serverAddress); + } else { + serverPorts = serverPort.toString(); + } + } } @Override @@ -128,10 +163,7 @@ public void applyFeatureSettings(AbstractBean other) { @Override public String displayAddress() { - if (HysteriaFmtKt.isMultiPort(this)) { - return serverAddress; - } - return super.displayAddress(); + return NetsKt.wrapIPV6Host(serverAddress) + ":" + serverPorts; } @Override @@ -157,4 +189,4 @@ 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 index c0e321c3f..1e78a29f5 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt @@ -4,24 +4,25 @@ import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.fmt.LOCALHOST import io.nekohasekai.sagernet.ktx.* import moe.matsuri.nb4a.SingBoxOptions +import moe.matsuri.nb4a.utils.listByLineOrComma import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.json.JSONObject import java.io.File // hysteria://host:port?auth=123456&peer=sni.domain&insecure=1|0&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks - -fun parseHysteria(url: String): HysteriaBean { +fun parseHysteria1(url: String): HysteriaBean { val link = url.replace("hysteria://", "https://").toHttpUrlOrNull() ?: error( "invalid hysteria link $url" ) return HysteriaBean().apply { + protocolVersion = 1 serverAddress = link.host - serverPort = link.port + serverPorts = link.port.toString() name = link.fragment link.queryParameter("mport")?.also { - serverAddress = serverAddress.wrapIPV6Host() + ":" + it + serverPorts = it } link.queryParameter("peer")?.also { sni = it @@ -31,7 +32,7 @@ fun parseHysteria(url: String): HysteriaBean { authPayload = it } link.queryParameter("insecure")?.also { - allowInsecure = it == "1" + allowInsecure = it == "1" || it == "true" } link.queryParameter("upmbps")?.also { uploadMbps = it.toIntOrNull() ?: uploadMbps @@ -50,6 +51,7 @@ fun parseHysteria(url: String): HysteriaBean { "faketcp" -> { protocol = HysteriaBean.PROTOCOL_FAKETCP } + "wechat-video" -> { protocol = HysteriaBean.PROTOCOL_WECHAT_VIDEO } @@ -58,53 +60,116 @@ fun parseHysteria(url: String): HysteriaBean { } } -fun HysteriaBean.toUri(): String { - val builder = linkBuilder().host(serverAddress.substringBeforeLast(":")).port(serverPort) - if (isMultiPort()) { - builder.addQueryParameter("mport", serverAddress.substringAfterLast(":")) - } - if (allowInsecure) { - builder.addQueryParameter("insecure", "1") +// hysteria2://[auth@]hostname[:port]/?[key=value]&[key=value]... +fun parseHysteria2(url: String): HysteriaBean { + val link = url + .replace("hysteria2://", "https://") + .replace("hy2://", "https://") + .toHttpUrlOrNull() ?: error("invalid hysteria link $url") + return HysteriaBean().apply { + protocolVersion = 2 + serverAddress = link.host + serverPorts = link.port.toString() + authPayload = if (link.password.isNotBlank()) { + link.username + ":" + link.password + } else { + link.username + } + name = link.fragment + + link.queryParameter("mport")?.also { + serverPorts = it + } + link.queryParameter("sni")?.also { + sni = it + } + link.queryParameter("insecure")?.also { + allowInsecure = it == "1" || it == "true" + } +// link.queryParameter("upmbps")?.also { +// uploadMbps = it.toIntOrNull() ?: uploadMbps +// } +// link.queryParameter("downmbps")?.also { +// downloadMbps = it.toIntOrNull() ?: downloadMbps +// } + link.queryParameter("obfs-password")?.also { + obfuscation = it + } +// link.queryParameter("pinSHA256")?.also { +// // TODO your box do not support it +// } } - if (sni.isNotBlank()) { - builder.addQueryParameter("peer", sni) +} + +fun HysteriaBean.toUri(): String { + var un = "" + var pw = "" + if (protocolVersion == 2) { + if (authPayload.contains(":")) { + un = authPayload.substringBefore(":") + pw = authPayload.substringAfter(":") + } else { + un = authPayload + } } - if (authPayload.isNotBlank()) { - builder.addQueryParameter("auth", authPayload) + // + val builder = linkBuilder() + .host(serverAddress) + .port(getFirstPort(serverPorts)) + .username(un) + .password(pw) + if (isMultiPort(displayAddress())) { + builder.addQueryParameter("mport", serverPorts) } - builder.addQueryParameter("upmbps", "$uploadMbps") - builder.addQueryParameter("downmbps", "$downloadMbps") - if (alpn.isNotBlank()) { - builder.addQueryParameter("alpn", alpn) + if (name.isNotBlank()) { + builder.encodedFragment(name.urlSafe()) } - if (obfuscation.isNotBlank()) { - builder.addQueryParameter("obfs", "xplus") - builder.addQueryParameter("obfsParam", obfuscation) + if (allowInsecure) { + builder.addQueryParameter("insecure", "1") } - when (protocol) { - HysteriaBean.PROTOCOL_FAKETCP -> { - builder.addQueryParameter("protocol", "faketcp") + if (protocolVersion == 1) { + if (sni.isNotBlank()) { + builder.addQueryParameter("peer", sni) } - HysteriaBean.PROTOCOL_WECHAT_VIDEO -> { - builder.addQueryParameter("protocol", "wechat-video") + if (authPayload.isNotBlank()) { + builder.addQueryParameter("auth", authPayload) + } + builder.addQueryParameter("upmbps", "$uploadMbps") + builder.addQueryParameter("downmbps", "$downloadMbps") + if (alpn.isNotBlank()) { + builder.addQueryParameter("alpn", alpn) + } + if (obfuscation.isNotBlank()) { + builder.addQueryParameter("obfs", "xplus") + builder.addQueryParameter("obfsParam", obfuscation) + } + when (protocol) { + HysteriaBean.PROTOCOL_FAKETCP -> { + builder.addQueryParameter("protocol", "faketcp") + } + + HysteriaBean.PROTOCOL_WECHAT_VIDEO -> { + builder.addQueryParameter("protocol", "wechat-video") + } + } + } else { + if (sni.isNotBlank()) { + builder.addQueryParameter("sni", sni) + } + if (obfuscation.isNotBlank()) { + builder.addQueryParameter("obfs", "salamander") + builder.addQueryParameter("obfs-password", obfuscation) } } - if (protocol == HysteriaBean.PROTOCOL_FAKETCP) { - builder.addQueryParameter("protocol", "faketcp") - } - if (name.isNotBlank()) { - builder.encodedFragment(name.urlSafe()) - } - return builder.toLink("hysteria") + return builder.toLink(if (protocolVersion == 2) "hy2" else "hysteria") } -fun JSONObject.parseHysteria(): HysteriaBean { +fun JSONObject.parseHysteria1Json(): HysteriaBean { + // TODO parse HY2 JSON+YAML return HysteriaBean().apply { - serverAddress = optString("server") - if (!isMultiPort()) { - serverAddress = optString("server").substringBeforeLast(":") - serverPort = optString("server").substringAfterLast(":").toIntOrNull() ?: 443 - } + protocolVersion = 1 + serverAddress = optString("server").substringBeforeLast(":") + serverPorts = optString("server").substringAfterLast(":") uploadMbps = getIntNya("up_mbps") downloadMbps = getIntNya("down_mbps") obfuscation = getStr("obfs") @@ -121,6 +186,7 @@ fun JSONObject.parseHysteria(): HysteriaBean { "faketcp" -> { protocol = HysteriaBean.PROTOCOL_FAKETCP } + "wechat-video" -> { protocol = HysteriaBean.PROTOCOL_WECHAT_VIDEO } @@ -136,13 +202,17 @@ fun JSONObject.parseHysteria(): HysteriaBean { } } -fun HysteriaBean.buildHysteriaConfig(port: Int, cacheFile: (() -> File)?): String { +fun HysteriaBean.buildHysteria1Config(port: Int, cacheFile: (() -> File)?): String { + if (protocolVersion != 1) { + throw Exception("error version: $protocolVersion") + } return JSONObject().apply { - put("server", if (isMultiPort()) serverAddress else wrapUri()) + put("server", displayAddress()) when (protocol) { HysteriaBean.PROTOCOL_FAKETCP -> { put("protocol", "faketcp") } + HysteriaBean.PROTOCOL_WECHAT_VIDEO -> { put("protocol", "wechat-video") } @@ -157,6 +227,8 @@ fun HysteriaBean.buildHysteriaConfig(port: Int, cacheFile: (() -> File)?): Strin ) ) put("retry", 5) + put("fast_open", true) + put("lazy_start", true) put("obfs", obfuscation) when (authPayloadType) { HysteriaBean.TYPE_BASE64 -> put("auth", authPayload) @@ -180,57 +252,117 @@ fun HysteriaBean.buildHysteriaConfig(port: Int, cacheFile: (() -> File)?): Strin if (connectionReceiveWindow > 0) put("recv_window", connectionReceiveWindow) if (disableMtuDiscovery) put("disable_mtu_discovery", true) - // hy 1.2.0 (不兼容) - put("resolver", "udp://127.0.0.1:" + DataStore.localDNSPort) - put("hop_interval", hopInterval) }.toStringPretty() } -fun HysteriaBean.isMultiPort(): Boolean { - if (!serverAddress.contains(":")) return false - val p = serverAddress.substringAfterLast(":") +fun isMultiPort(hyAddr: String): Boolean { + if (!hyAddr.contains(":")) return false + val p = hyAddr.substringAfterLast(":") if (p.contains("-") || p.contains(",")) return true return false } +fun getFirstPort(portStr: String): Int { + return portStr.substringBefore(":").substringBefore(",").toIntOrNull() ?: 443 +} + fun HysteriaBean.canUseSingBox(): Boolean { - if (isMultiPort() || protocol != HysteriaBean.PROTOCOL_UDP) return false + if (protocol != HysteriaBean.PROTOCOL_UDP) return false return true } -fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): SingBoxOptions.Outbound_HysteriaOptions { - // No multi-port - return SingBoxOptions.Outbound_HysteriaOptions().apply { - type = "hysteria" - server = bean.serverAddress - server_port = bean.serverPort - up_mbps = bean.uploadMbps - down_mbps = bean.downloadMbps - obfs = bean.obfuscation - disable_mtu_discovery = bean.disableMtuDiscovery - when (bean.authPayloadType) { - HysteriaBean.TYPE_BASE64 -> auth = bean.authPayload - HysteriaBean.TYPE_STRING -> auth_str = bean.authPayload - } - if (bean.streamReceiveWindow > 0) { - recv_window_conn = bean.streamReceiveWindow.toLong() - } - if (bean.connectionReceiveWindow > 0) { - recv_window_conn = bean.connectionReceiveWindow.toLong() - } - tls = SingBoxOptions.OutboundTLSOptions().apply { - if (bean.sni.isNotBlank()) { - server_name = bean.sni +fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): SingBoxOptions.SingBoxOption { + return when (bean.protocolVersion) { + 1 -> SingBoxOptions.Outbound_HysteriaOptions().apply { + type = "hysteria" + server = bean.serverAddress + val port = bean.serverPorts.toIntOrNull() + if (port != null) { + server_port = port + } else { + server_ports = hopPortsToSingboxList(bean.serverPorts) + } + hop_interval = "${bean.hopInterval}s" + up_mbps = bean.uploadMbps + down_mbps = bean.downloadMbps + obfs = bean.obfuscation + disable_mtu_discovery = bean.disableMtuDiscovery + when (bean.authPayloadType) { + HysteriaBean.TYPE_BASE64 -> auth = bean.authPayload + HysteriaBean.TYPE_STRING -> auth_str = bean.authPayload + } + if (bean.streamReceiveWindow > 0) { + recv_window_conn = bean.streamReceiveWindow.toLong() + } + if (bean.connectionReceiveWindow > 0) { + recv_window_conn = bean.connectionReceiveWindow.toLong() + } + tls = SingBoxOptions.OutboundTLSOptions().apply { + if (bean.sni.isNotBlank()) { + server_name = bean.sni + } + if (bean.alpn.isNotBlank()) { + alpn = bean.alpn.listByLineOrComma() + } + if (bean.caText.isNotBlank()) { + certificate = bean.caText + } + insecure = bean.allowInsecure || DataStore.globalAllowInsecure + enabled = true + } + } + + 2 -> SingBoxOptions.Outbound_Hysteria2Options().apply { + type = "hysteria2" + server = bean.serverAddress + val port = bean.serverPorts.toIntOrNull() + if (port != null) { + server_port = port + } else { + server_ports = hopPortsToSingboxList(bean.serverPorts) } - if (bean.alpn.isNotBlank()) { - alpn = bean.alpn.split("\n") + hop_interval = "${bean.hopInterval}s" + up_mbps = bean.uploadMbps + down_mbps = bean.downloadMbps + if (bean.obfuscation.isNotBlank()) { + obfs = SingBoxOptions.Hysteria2Obfs().apply { + type = "salamander" + password = bean.obfuscation + } } - if (bean.caText.isNotBlank()) { - certificate = bean.caText +// disable_mtu_discovery = bean.disableMtuDiscovery + password = bean.authPayload +// if (bean.streamReceiveWindow > 0) { +// recv_window_conn = bean.streamReceiveWindow.toLong() +// } +// if (bean.connectionReceiveWindow > 0) { +// recv_window_conn = bean.connectionReceiveWindow.toLong() +// } + tls = SingBoxOptions.OutboundTLSOptions().apply { + if (bean.sni.isNotBlank()) { + server_name = bean.sni + } + alpn = listOf("h3") + if (bean.caText.isNotBlank()) { + certificate = bean.caText + } + insecure = bean.allowInsecure || DataStore.globalAllowInsecure + enabled = true } - insecure = bean.allowInsecure - enabled = true + } + + else -> error("error_version $bean.protocolVersion") + } +} + +fun hopPortsToSingboxList(s: String): List { + return s.split(",").mapNotNull { + val pRange = it.replace("-", ":") + if (pRange.split(":").size == 2) { + pRange + } else { + null } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java new file mode 100644 index 000000000..0a7c21fab --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java @@ -0,0 +1,89 @@ +/****************************************************************************** + * Copyright (C) 2022 by nekohasekai * + * * + * 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.mieru; + +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 MieruBean extends AbstractBean { + + public String protocol; + public String username; + public String password; + public Integer mtu; + + @Override + public void initializeDefaultValues() { + super.initializeDefaultValues(); + if (protocol == null) protocol = "TCP"; + if (username == null) username = ""; + if (password == null) password = ""; + if (mtu == null) mtu = 1400; + } + + @Override + public void serialize(ByteBufferOutput output) { + output.writeInt(0); + super.serialize(output); + output.writeString(protocol); + output.writeString(username); + output.writeString(password); + if (protocol.equals("UDP")) { + output.writeInt(mtu); + } + } + + @Override + public void deserialize(ByteBufferInput input) { + int version = input.readInt(); + super.deserialize(input); + protocol = input.readString(); + username = input.readString(); + password = input.readString(); + if (protocol.equals("UDP")) { + mtu = input.readInt(); + } + } + + @NotNull + @Override + public MieruBean clone() { + return KryoConverters.deserialize(new MieruBean(), KryoConverters.serialize(this)); + } + + public static final Creator CREATOR = new CREATOR() { + @NonNull + @Override + public MieruBean newInstance() { + return new MieruBean(); + } + + @Override + public MieruBean[] newArray(int size) { + return new MieruBean[size]; + } + }; +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt new file mode 100644 index 000000000..476999a0b --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt @@ -0,0 +1,54 @@ +/****************************************************************************** + * Copyright (C) 2022 by nekohasekai * + * * + * 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.mieru + +import io.nekohasekai.sagernet.ktx.toStringPretty +import org.json.JSONArray +import org.json.JSONObject + +fun MieruBean.buildMieruConfig(port: Int): String { + val serverInfo = JSONArray().apply { + put(JSONObject().apply { + put("ipAddress", finalAddress) + put("portBindings", JSONArray().apply { + put(JSONObject().apply { + put("port", finalPort) + put("protocol", protocol) + }) + }) + }) + } + return JSONObject().apply { + put("activeProfile", "default") + put("socks5Port", port) + // TODO: follow NekoBox logging level. + put("loggingLevel", "INFO") + put("profiles", JSONArray().apply { + put(JSONObject().apply { + put("profileName", "default") + put("user", JSONObject().apply { + put("name", username) + put("password", password) + }) + put("servers", serverInfo) + put("mtu", mtu) + }) + }) + }.toStringPretty() +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveBean.java index 841ae59a8..6e504d4a5 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveBean.java @@ -23,6 +23,9 @@ public class NaiveBean extends AbstractBean { public String certificates; public Integer insecureConcurrency; + // sing-box socks + public Boolean sUoT; + @Override public void initializeDefaultValues() { if (serverPort == null) serverPort = 443; @@ -34,11 +37,12 @@ public void initializeDefaultValues() { if (certificates == null) certificates = ""; if (sni == null) sni = ""; if (insecureConcurrency == null) insecureConcurrency = 0; + if (sUoT == null) sUoT = false; } @Override public void serialize(ByteBufferOutput output) { - output.writeInt(2); + output.writeInt(3); super.serialize(output); output.writeString(proto); output.writeString(username); @@ -48,6 +52,7 @@ public void serialize(ByteBufferOutput output) { output.writeString(certificates); output.writeString(sni); output.writeInt(insecureConcurrency); + output.writeBoolean(sUoT); } @Override @@ -65,6 +70,9 @@ public void deserialize(ByteBufferInput input) { if (version >= 1) { insecureConcurrency = input.readInt(); } + if (version >= 3) { + sUoT = input.readBoolean(); + } } @NotNull diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java index 10c394f59..326662ffb 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java @@ -50,6 +50,13 @@ public void deserialize(ByteBufferInput input) { sUoT = input.readBoolean(); } + @Override + public void applyFeatureSettings(AbstractBean other) { + if (!(other instanceof ShadowsocksBean)) return; + ShadowsocksBean bean = ((ShadowsocksBean) other); + bean.sUoT = sUoT; + } + @NotNull @Override public ShadowsocksBean clone() { 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 9b0c61639..b1d0bb9c1 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 @@ -114,14 +114,13 @@ fun buildSingBoxOutboundShadowsocksBean(bean: ShadowsocksBean): SingBoxOptions.O server_port = bean.serverPort password = bean.password method = bean.method - if (bean.sUoT) { - udp_over_tcp = SingBoxOptions.UDPOverTCPOptions().apply { - enabled = true - } - } if (bean.plugin.isNotBlank()) { plugin = bean.plugin.substringBefore(";") plugin_opts = bean.plugin.substringAfter(";") + if (plugin == "none") { + plugin = null + plugin_opts = null + } } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java index fb07ac109..5780480d7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java @@ -14,6 +14,8 @@ public class SOCKSBean extends AbstractBean { public Integer protocol; + public Boolean sUoT; + public int protocolVersion() { switch (protocol) { case 0: @@ -66,15 +68,17 @@ public void initializeDefaultValues() { if (protocol == null) protocol = PROTOCOL_SOCKS5; if (username == null) username = ""; if (password == null) password = ""; + if (sUoT == null) sUoT = false; } @Override public void serialize(ByteBufferOutput output) { - output.writeInt(1); + output.writeInt(2); super.serialize(output); output.writeInt(protocol); output.writeString(username); output.writeString(password); + output.writeBoolean(sUoT); } @Override @@ -86,6 +90,9 @@ public void deserialize(ByteBufferInput input) { } username = input.readString(); password = input.readString(); + if (version >= 2) { + sUoT = input.readBoolean(); + } } @NotNull diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt index 8eee29106..b3aff52d7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt @@ -6,42 +6,33 @@ import io.nekohasekai.sagernet.ktx.unUrlSafe import io.nekohasekai.sagernet.ktx.urlSafe import moe.matsuri.nb4a.SingBoxOptions import moe.matsuri.nb4a.utils.NGUtil +import moe.matsuri.nb4a.utils.Util import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull fun parseSOCKS(link: String): SOCKSBean { - if (!link.substringAfter("://").contains(":")) { - // v2rayN shit format - var url = link.substringAfter("://") - if (url.contains("#")) { - url = url.substringBeforeLast("#") - } - url = url.decodeBase64UrlSafe() - val httpUrl = "http://$url".toHttpUrlOrNull() ?: error("Invalid v2rayN link content: $url") - return SOCKSBean().apply { - serverAddress = httpUrl.host - serverPort = httpUrl.port - username = httpUrl.username.takeIf { it != "null" } ?: "" - password = httpUrl.password.takeIf { it != "null" } ?: "" - if (link.contains("#")) { - name = link.substringAfter("#").unUrlSafe() - } - } - } else { - val url = ("http://" + link.substringAfter("://")).toHttpUrlOrNull() - ?: error("Not supported: $link") + val url = ("http://" + link.substringAfter("://")).toHttpUrlOrNull() + ?: error("Not supported: $link") - return SOCKSBean().apply { - protocol = when { - link.startsWith("socks4://") -> SOCKSBean.PROTOCOL_SOCKS4 - link.startsWith("socks4a://") -> SOCKSBean.PROTOCOL_SOCKS4A - else -> SOCKSBean.PROTOCOL_SOCKS5 + return SOCKSBean().apply { + protocol = when { + link.startsWith("socks4://") -> SOCKSBean.PROTOCOL_SOCKS4 + link.startsWith("socks4a://") -> SOCKSBean.PROTOCOL_SOCKS4A + else -> SOCKSBean.PROTOCOL_SOCKS5 + } + name = url.fragment + serverAddress = url.host + serverPort = url.port + username = url.username + password = url.password + // v2rayN fmt + if (password.isNullOrBlank() && !username.isNullOrBlank()) { + try { + val n = username.decodeBase64UrlSafe() + username = n.substringBefore(":") + password = n.substringAfter(":") + } catch (_: Exception) { } - serverAddress = url.host - serverPort = url.port - username = url.username - password = url.password - name = url.fragment } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHFmt.kt index 4ee032a24..170e640f4 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHFmt.kt @@ -1,6 +1,7 @@ package io.nekohasekai.sagernet.fmt.ssh import moe.matsuri.nb4a.SingBoxOptions +import moe.matsuri.nb4a.utils.listByLineOrComma fun buildSingBoxOutboundSSHBean(bean: SSHBean): SingBoxOptions.Outbound_SSHOptions { return SingBoxOptions.Outbound_SSHOptions().apply { @@ -9,7 +10,7 @@ fun buildSingBoxOutboundSSHBean(bean: SSHBean): SingBoxOptions.Outbound_SSHOptio server_port = bean.serverPort user = bean.username if (bean.publicKey.isNotBlank()) { - host_key = bean.publicKey.split("\n") + host_key = bean.publicKey.listByLineOrComma() } when (bean.authType) { SSHBean.AUTH_TYPE_PRIVATE_KEY -> { diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt index cb0235754..4e6e91f45 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt @@ -92,10 +92,10 @@ fun TrojanGoBean.buildTrojanGoConfig(port: Int): String { put(password) }) put("log_level", if (DataStore.logLevel > 0) 0 else 2) - if (Protocols.shouldEnableMux("trojan-go")) put("mux", JSONObject().apply { - put("enabled", true) - put("concurrency", DataStore.muxConcurrency) - }) +// if (Protocols.shouldEnableMux("trojan-go")) put("mux", JSONObject().apply { +// put("enabled", true) +// put("concurrency", DataStore.muxConcurrency) +// }) put("tcp", JSONObject().apply { put("prefer_ipv4", DataStore.ipv6Mode <= IPv6Mode.ENABLE) }) diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java index 4ce148f46..f1b76d369 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java @@ -21,9 +21,18 @@ public class TuicBean extends AbstractBean { public Boolean reduceRTT; public Integer mtu; public String sni; + + // TUIC zep + public Boolean fastConnect; public Boolean allowInsecure; + // TUIC v5 + + public String customJSON; + public Integer protocolVersion; + public String uuid; + @Override public void initializeDefaultValues() { super.initializeDefaultValues(); @@ -38,11 +47,14 @@ public void initializeDefaultValues() { if (sni == null) sni = ""; if (fastConnect == null) fastConnect = false; if (allowInsecure == null) allowInsecure = false; + if (customJSON == null) customJSON = ""; + if (protocolVersion == null) protocolVersion = 5; + if (uuid == null) uuid = ""; } @Override public void serialize(ByteBufferOutput output) { - output.writeInt(1); + output.writeInt(2); super.serialize(output); output.writeString(token); output.writeString(caText); @@ -55,6 +67,9 @@ public void serialize(ByteBufferOutput output) { output.writeString(sni); output.writeBoolean(fastConnect); output.writeBoolean(allowInsecure); + output.writeString(customJSON); + output.writeInt(protocolVersion); + output.writeString(uuid); } @Override @@ -74,6 +89,13 @@ public void deserialize(ByteBufferInput input) { fastConnect = input.readBoolean(); allowInsecure = input.readBoolean(); } + if (version >= 2) { + customJSON = input.readString(); + protocolVersion = input.readInt(); + uuid = input.readString(); + } else { + protocolVersion = 4; + } } @Override diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt index ce3136378..cad2147e6 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt @@ -1,64 +1,89 @@ package io.nekohasekai.sagernet.fmt.tuic import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.fmt.LOCALHOST -import io.nekohasekai.sagernet.ktx.isIpAddress -import io.nekohasekai.sagernet.ktx.toStringPretty -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import moe.matsuri.nb4a.plugin.Plugins -import org.json.JSONArray -import org.json.JSONObject -import java.io.File -import java.net.InetAddress +import io.nekohasekai.sagernet.ktx.linkBuilder +import io.nekohasekai.sagernet.ktx.toLink +import io.nekohasekai.sagernet.ktx.urlSafe +import moe.matsuri.nb4a.SingBoxOptions +import moe.matsuri.nb4a.utils.listByLineOrComma +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String { - if (Plugins.isUsingMatsuriExe("tuic-plugin")) { - if (!serverAddress.isIpAddress()) { - runBlocking { - finalAddress = withContext(Dispatchers.IO) { - InetAddress.getAllByName(serverAddress) - }?.firstOrNull()?.hostAddress ?: "127.0.0.1" - // TODO network on main thread, tuic don't support "sni" - } +fun parseTuic(url: String): TuicBean { + // https://github.com/daeuniverse/dae/discussions/182 + val link = url.replace("tuic://", "https://").toHttpUrlOrNull() ?: error( + "invalid tuic link $url" + ) + return TuicBean().apply { + protocolVersion = 5 + + name = link.fragment + uuid = link.username + token = link.password + serverAddress = link.host + serverPort = link.port + + link.queryParameter("sni")?.let { + sni = it + } + link.queryParameter("congestion_control")?.let { + congestionController = it + } + link.queryParameter("udp_relay_mode")?.let { + udpRelayMode = it + } + link.queryParameter("alpn")?.let { + alpn = it + } + link.queryParameter("allow_insecure")?.let { + if (it == "1") allowInsecure = true + } + link.queryParameter("disable_sni")?.let { + if (it == "1") disableSNI = true } } - return JSONObject().apply { - put("relay", JSONObject().apply { - if (sni.isNotBlank()) { - put("server", sni) - put("ip", finalAddress) - } else if (serverAddress.isIpAddress()) { - put("server", finalAddress) - } else { - put("server", serverAddress) - put("ip", finalAddress) - } - put("port", finalPort) - put("token", token) +} - if (caText.isNotBlank() && cacheFile != null) { - val caFile = cacheFile() - caFile.writeText(caText) - put("certificates", JSONArray(listOf(caFile.absolutePath))) - } +fun TuicBean.toUri(): String { + val builder = linkBuilder().username(uuid).password(token).host(serverAddress).port(serverPort) + + builder.addQueryParameter("congestion_control", congestionController) + builder.addQueryParameter("udp_relay_mode", udpRelayMode) + + if (sni.isNotBlank()) builder.addQueryParameter("sni", sni) + if (alpn.isNotBlank()) builder.addQueryParameter("alpn", alpn) + if (allowInsecure) builder.addQueryParameter("allow_insecure", "1") + if (disableSNI) builder.addQueryParameter("disable_sni", "1") + if (name.isNotBlank()) builder.encodedFragment(name.urlSafe()) + + return builder.toLink("tuic") +} - put("udp_relay_mode", udpRelayMode) - if (alpn.isNotBlank()) { - put("alpn", JSONArray(alpn.split("\n"))) +fun buildSingBoxOutboundTuicBean(bean: TuicBean): SingBoxOptions.Outbound_TUICOptions { + if (bean.protocolVersion == 4) throw Exception("TUIC v4 is no longer supported") + return SingBoxOptions.Outbound_TUICOptions().apply { + type = "tuic" + server = bean.serverAddress + server_port = bean.serverPort + uuid = bean.uuid + password = bean.token + congestion_control = bean.congestionController + when (bean.udpRelayMode) { + "quic" -> udp_relay_mode = "quic" + } + zero_rtt_handshake = bean.reduceRTT + tls = SingBoxOptions.OutboundTLSOptions().apply { + if (bean.sni.isNotBlank()) { + server_name = bean.sni + } + if (bean.alpn.isNotBlank()) { + alpn = bean.alpn.listByLineOrComma() + } + if (bean.caText.isNotBlank()) { + certificate = bean.caText } - put("congestion_controller", congestionController) - put("disable_sni", disableSNI) - put("reduce_rtt", reduceRTT) - put("max_udp_relay_packet_size", mtu) - if (fastConnect) put("fast_connect", true) - if (allowInsecure) put("insecure", true) - }) - put("local", JSONObject().apply { - put("ip", LOCALHOST) - put("port", port) - }) - put("log_level", if (DataStore.logLevel > 0) "debug" else "info") - }.toStringPretty() + disable_sni = bean.disableSNI + insecure = bean.allowInsecure || DataStore.globalAllowInsecure + enabled = true + } + } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java index 808c90e5d..2f32c50f4 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java @@ -14,7 +14,7 @@ public abstract class StandardV2RayBean extends AbstractBean { //////// End of VMess & VLESS //////// - // "V2Ray Transport" tcp/http/ws/quic/grpc + // "V2Ray Transport" tcp/http/ws/quic/grpc/httpupgrade public String type; public String host; @@ -48,6 +48,20 @@ public abstract class StandardV2RayBean extends AbstractBean { public String certificates; + // --------------------------------------- ech + + public Boolean enableECH; + + public String echConfig; + + // --------------------------------------- Mux + + public Boolean enableMux; + public Boolean muxPadding; + public Integer muxType; + public Integer muxConcurrency; + + // --------------------------------------- // public Integer packetEncoding; // 1:packet 2:xudp @@ -61,11 +75,13 @@ public void initializeDefaultValues() { if (JavaUtil.isNullOrBlank(type)) type = "tcp"; else if ("h2".equals(type)) type = "http"; + type = type.toLowerCase(); + if (JavaUtil.isNullOrBlank(host)) host = ""; if (JavaUtil.isNullOrBlank(path)) path = ""; if (JavaUtil.isNullOrBlank(security)) { - if (this instanceof TrojanBean || isVLESS()) { + if (this instanceof TrojanBean) { security = "tls"; } else { security = "none"; @@ -84,13 +100,20 @@ public void initializeDefaultValues() { if (realityPubKey == null) realityPubKey = ""; if (realityShortId == null) realityShortId = ""; + + if (enableECH == null) enableECH = false; + if (JavaUtil.isNullOrBlank(echConfig)) echConfig = ""; + + if (enableMux == null) enableMux = false; + if (muxPadding == null) muxPadding = false; + if (muxType == null) muxType = 0; + if (muxConcurrency == null) muxConcurrency = 1; } @Override public void serialize(ByteBufferOutput output) { - output.writeInt(0); + output.writeInt(4); super.serialize(output); - output.writeString(uuid); output.writeString(encryption); if (this instanceof VMessBean) { @@ -110,13 +133,15 @@ public void serialize(ByteBufferOutput output) { output.writeString(earlyDataHeaderName); break; } - case "http": { + case "http": + case "httpupgrade": { output.writeString(host); output.writeString(path); break; } case "grpc": { output.writeString(path); + break; } } @@ -131,7 +156,15 @@ public void serialize(ByteBufferOutput output) { output.writeString(realityShortId); } + output.writeBoolean(enableECH); + output.writeString(echConfig); + output.writeInt(packetEncoding); + + output.writeBoolean(enableMux); + output.writeBoolean(muxPadding); + output.writeInt(muxType); + output.writeInt(muxConcurrency); } @Override @@ -157,13 +190,20 @@ public void deserialize(ByteBufferInput input) { earlyDataHeaderName = input.readString(); break; } - case "http": { + case "http": + case "httpupgrade": { host = input.readString(); path = input.readString(); break; } case "grpc": { path = input.readString(); + if (version < 4) { + // 解决老版本数据的读取问题 + input.readString(); + input.readString(); + } + break; } } @@ -178,7 +218,44 @@ public void deserialize(ByteBufferInput input) { realityShortId = input.readString(); } + if (version >= 1) { + enableECH = input.readBoolean(); + if (version >= 3) { + echConfig = input.readString(); + } else { + if (enableECH) { + input.readBoolean(); + input.readBoolean(); + echConfig = input.readString(); + } + } + } else if (version == 0) { + // 从老版本升级上来但是 version == 0, 可能有 enableECH 也可能没有,需要做判断 + int position = input.getByteBuffer().position(); // 当前位置 + + boolean tmpEnableECH = input.readBoolean(); + int tmpPacketEncoding = input.readInt(); + + input.setPosition(position); // 读后归位 + + if (tmpPacketEncoding != 1 && tmpPacketEncoding != 2) { + enableECH = tmpEnableECH; + if (enableECH) { + input.readBoolean(); + input.readBoolean(); + echConfig = input.readString(); + } + } // 否则后一位就是 packetEncoding + } + packetEncoding = input.readInt(); + + if (version >= 2) { + enableMux = input.readBoolean(); + muxPadding = input.readBoolean(); + muxType = input.readInt(); + muxConcurrency = input.readInt(); + } } @Override @@ -187,8 +264,13 @@ public void applyFeatureSettings(AbstractBean other) { StandardV2RayBean bean = ((StandardV2RayBean) other); bean.allowInsecure = allowInsecure; bean.utlsFingerprint = utlsFingerprint; - bean.realityPubKey = realityPubKey; - bean.realityShortId = realityShortId; + bean.packetEncoding = packetEncoding; + bean.enableECH = enableECH; + bean.echConfig = echConfig; + bean.enableMux = enableMux; + bean.muxPadding = muxPadding; + bean.muxType = muxType; + bean.muxConcurrency = muxConcurrency; } public boolean isVLESS() { diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt index 20e63202b..93684a5b3 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt @@ -2,11 +2,13 @@ package io.nekohasekai.sagernet.fmt.v2ray import android.text.TextUtils import com.google.gson.Gson +import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.fmt.http.HttpBean import io.nekohasekai.sagernet.fmt.trojan.TrojanBean import io.nekohasekai.sagernet.ktx.* import moe.matsuri.nb4a.SingBoxOptions.* import moe.matsuri.nb4a.utils.NGUtil +import moe.matsuri.nb4a.utils.listByLineOrComma import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import org.json.JSONObject @@ -100,6 +102,7 @@ fun parseV2Ray(link: String): StandardV2RayBean { bean.host = it.split("|").joinToString(",") } } + "ws" -> { url.queryParameter("path")?.let { bean.path = it @@ -108,11 +111,21 @@ fun parseV2Ray(link: String): StandardV2RayBean { bean.host = it } } + "grpc" -> { url.queryParameter("serviceName")?.let { bean.path = it } } + + "httpupgrade" -> { + url.queryParameter("path")?.let { + bean.path = it + } + url.queryParameter("host")?.let { + bean.host = it + } + } } } else { // also vless format @@ -140,21 +153,27 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) { } type = url.queryParameter("type") ?: "tcp" - if (type == "h2") type = "http" + if (type == "h2" || url.queryParameter("headerType") == "http") type = "http" security = url.queryParameter("security") - if (security == null) { + if (security.isNullOrBlank()) { security = if (this is TrojanBean) "tls" else "none" } when (security) { "tls", "reality" -> { security = "tls" + url.queryParameter("allowInsecure")?.let { + allowInsecure = it == "1" || it == "true" + } url.queryParameter("sni")?.let { sni = it } + url.queryParameter("host")?.let { + if (sni.isNullOrBlank()) sni = it + } url.queryParameter("alpn")?.let { - alpn = it.replace(",", "\n") + alpn = it } url.queryParameter("cert")?.let { certificates = it @@ -167,16 +186,8 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) { } } } + when (type) { - "tcp" -> { - // v2rayNG - if (url.queryParameter("headerType") == "http") { - url.queryParameter("host")?.let { - type = "http" - host = it - } - } - } "http" -> { url.queryParameter("host")?.let { host = it @@ -185,6 +196,7 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) { path = it } } + "ws" -> { url.queryParameter("host")?.let { host = it @@ -200,11 +212,21 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) { } } } + "grpc" -> { url.queryParameter("serviceName")?.let { path = it } } + + "httpupgrade" -> { + url.queryParameter("host")?.let { + host = it + } + url.queryParameter("path")?.let { + path = it + } + } } // maybe from matsuri vmess exoprt @@ -222,8 +244,8 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) { } url.queryParameter("flow")?.let { - if (isVLESS && it.contains("vision")) { - encryption = it + if (isVLESS) { + encryption = it.removeSuffix("-udp443") } } @@ -302,6 +324,7 @@ fun parseV2RayN(link: String): VMessBean { throw Exception("invalid VmessQRCode") } + bean.name = vmessQRCode.ps bean.serverAddress = vmessQRCode.add bean.serverPort = vmessQRCode.port.toIntOrNull() bean.encryption = vmessQRCode.scy @@ -320,14 +343,15 @@ fun parseV2RayN(link: String): VMessBean { } } when (vmessQRCode.tls) { - "tls", "reality" -> bean.security = "tls" + "tls", "reality" -> { + bean.security = "tls" + bean.sni = vmessQRCode.sni + if (bean.sni.isNullOrBlank()) bean.sni = bean.host + bean.alpn = vmessQRCode.alpn + bean.utlsFingerprint = vmessQRCode.fp + } } - bean.name = vmessQRCode.ps - bean.sni = vmessQRCode.sni - bean.alpn = vmessQRCode.alpn.replace(",", "\n") - bean.utlsFingerprint = vmessQRCode.fp - return bean } @@ -424,7 +448,7 @@ fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String { when (type) { "tcp" -> {} - "ws", "http" -> { + "ws", "http", "httpupgrade" -> { if (host.isNotBlank()) { builder.addQueryParameter("host", host) } @@ -443,6 +467,7 @@ fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String { builder.addQueryParameter("headerType", "http") } } + "grpc" -> { if (path.isNotBlank()) { builder.setQueryParameter("serviceName", path) @@ -480,8 +505,9 @@ fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String { when (packetEncoding) { 1 -> { - builder.addQueryParameter("packetEncoding", "packet") + builder.addQueryParameter("packetEncoding", "packetaddr") } + 2 -> { builder.addQueryParameter("packetEncoding", "xudp") } @@ -499,6 +525,7 @@ fun buildSingBoxOutboundStreamSettings(bean: StandardV2RayBean): V2RayTransportO "tcp" -> { return null } + "ws" -> { return V2RayTransportOptions_WebsocketOptions().apply { type = "ws" @@ -525,33 +552,39 @@ fun buildSingBoxOutboundStreamSettings(bean: StandardV2RayBean): V2RayTransportO } } } + "http" -> { return V2RayTransportOptions_HTTPOptions().apply { type = "http" + if (!bean.isTLS()) method = "GET" // v2ray tcp header if (bean.host.isNotBlank()) { host = bean.host.split(",") } path = bean.path.takeIf { it.isNotBlank() } ?: "/" } } + "quic" -> { return V2RayTransportOptions().apply { type = "quic" } } + "grpc" -> { return V2RayTransportOptions_GRPCOptions().apply { type = "grpc" service_name = bean.path } } - } -// if (needKeepAliveInterval) { -// sockopt = StreamSettingsObject.SockoptObject().apply { -// tcpKeepAliveInterval = keepAliveInterval -// } -// } + "httpupgrade" -> { + return V2RayTransportOptions_HTTPUpgradeOptions().apply { + type = "httpupgrade" + host = bean.host + path = bean.path + } + } + } return null } @@ -560,22 +593,32 @@ fun buildSingBoxOutboundTLS(bean: StandardV2RayBean): OutboundTLSOptions? { if (bean.security != "tls") return null return OutboundTLSOptions().apply { enabled = true - insecure = bean.allowInsecure + insecure = bean.allowInsecure || DataStore.globalAllowInsecure if (bean.sni.isNotBlank()) server_name = bean.sni - if (bean.alpn.isNotBlank()) alpn = bean.alpn.split("\n") + if (bean.alpn.isNotBlank()) alpn = bean.alpn.listByLineOrComma() if (bean.certificates.isNotBlank()) certificate = bean.certificates - if (bean.utlsFingerprint.isNotBlank()) { - utls = OutboundUTLSOptions().apply { - enabled = true - fingerprint = bean.utlsFingerprint - } - } + var fp = bean.utlsFingerprint if (bean.realityPubKey.isNotBlank()) { reality = OutboundRealityOptions().apply { enabled = true public_key = bean.realityPubKey short_id = bean.realityShortId } + if (fp.isNullOrBlank()) fp = "chrome" + } + if (fp.isNotBlank()) { + utls = OutboundUTLSOptions().apply { + enabled = true + fingerprint = fp + } + } + if (bean.enableECH) { + ech = OutboundECHOptions().apply { + enabled = true + if (bean.echConfig.isNotBlank()) { + config = bean.echConfig.lines() + } + } } } } @@ -592,6 +635,7 @@ fun buildSingBoxOutboundStandardV2RayBean(bean: StandardV2RayBean): Outbound { tls = buildSingBoxOutboundTLS(bean) } } + is VMessBean -> { if (bean.isVLESS) return Outbound_VLESSOptions().apply { type = "vless" @@ -625,6 +669,7 @@ fun buildSingBoxOutboundStandardV2RayBean(bean: StandardV2RayBean): Outbound { transport = buildSingBoxOutboundStreamSettings(bean) } } + is TrojanBean -> { return Outbound_TrojanOptions().apply { type = "trojan" @@ -635,6 +680,7 @@ fun buildSingBoxOutboundStandardV2RayBean(bean: StandardV2RayBean): Outbound { transport = buildSingBoxOutboundStreamSettings(bean) } } + else -> throw IllegalStateException("can't reach") } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessBean.java index 84044da1b..2b7e8ae9e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessBean.java @@ -16,7 +16,12 @@ public void initializeDefaultValues() { super.initializeDefaultValues(); alterId = alterId != null ? alterId : 0; - encryption = JavaUtil.isNotBlank(encryption) ? encryption : "auto"; + + if (alterId == -1) { + encryption = JavaUtil.isNotBlank(encryption) ? encryption : ""; + } else { + encryption = JavaUtil.isNotBlank(encryption) ? encryption : "auto"; + } } @NotNull diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt index 8e8388649..5fa7397b0 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt @@ -1,17 +1,41 @@ package io.nekohasekai.sagernet.fmt.wireguard import moe.matsuri.nb4a.SingBoxOptions +import moe.matsuri.nb4a.utils.Util +import moe.matsuri.nb4a.utils.listByLineOrComma + +fun genReserved(anyStr: String): String { + try { + val list = anyStr.listByLineOrComma() + val ba = ByteArray(3) + if (list.size == 3) { + list.forEachIndexed { index, s -> + val i = s + .replace("[", "") + .replace("]", "") + .replace(" ", "") + .toIntOrNull() ?: return anyStr + ba[index] = i.toByte() + } + return Util.b64EncodeOneLine(ba) + } else { + return anyStr + } + } catch (e: Exception) { + return anyStr + } +} fun buildSingBoxOutboundWireguardBean(bean: WireGuardBean): SingBoxOptions.Outbound_WireGuardOptions { return SingBoxOptions.Outbound_WireGuardOptions().apply { type = "wireguard" server = bean.serverAddress server_port = bean.serverPort - local_address = bean.localAddress.split("\n") + local_address = bean.localAddress.listByLineOrComma() private_key = bean.privateKey peer_public_key = bean.peerPublicKey pre_shared_key = bean.peerPreSharedKey mtu = bean.mtu - if (bean.reserved.isNotBlank()) reserved = bean.reserved + if (bean.reserved.isNotBlank()) reserved = genReserved(bean.reserved) } } 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 af97d1e69..28213489a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt @@ -1,18 +1,19 @@ package io.nekohasekai.sagernet.group import android.annotation.SuppressLint -import android.net.Uri import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.* import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.http.HttpBean import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean -import io.nekohasekai.sagernet.fmt.hysteria.parseHysteria +import io.nekohasekai.sagernet.fmt.hysteria.parseHysteria1Json import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks import io.nekohasekai.sagernet.fmt.socks.SOCKSBean import io.nekohasekai.sagernet.fmt.trojan.TrojanBean import io.nekohasekai.sagernet.fmt.trojan_go.parseTrojanGo +import io.nekohasekai.sagernet.fmt.tuic.TuicBean +import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean import io.nekohasekai.sagernet.fmt.v2ray.VMessBean import io.nekohasekai.sagernet.fmt.v2ray.isTLS import io.nekohasekai.sagernet.fmt.v2ray.setTLS @@ -20,7 +21,9 @@ import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean import io.nekohasekai.sagernet.ktx.* import libcore.Libcore import moe.matsuri.nb4a.Protocols +import moe.matsuri.nb4a.proxy.anytls.AnyTLSBean import moe.matsuri.nb4a.proxy.config.ConfigBean +import moe.matsuri.nb4a.utils.Util import org.ini4j.Ini import org.json.JSONArray import org.json.JSONObject @@ -29,6 +32,7 @@ import org.yaml.snakeyaml.TypeDescription import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.error.YAMLException import java.io.StringReader +import androidx.core.net.toUri @Suppress("EXPERIMENTAL_API_USAGE") object RawUpdater : GroupUpdater() { @@ -44,7 +48,7 @@ object RawUpdater : GroupUpdater() { val link = subscription.link var proxies: List if (link.startsWith("content://")) { - val contentText = app.contentResolver.openInputStream(Uri.parse(link)) + val contentText = app.contentResolver.openInputStream(link.toUri()) ?.bufferedReader() ?.readText() @@ -54,18 +58,33 @@ object RawUpdater : GroupUpdater() { val response = Libcore.newHttpClient().apply { trySocks5(DataStore.mixedPort) + tryH3Direct() when (DataStore.appTLSVersion) { "1.3" -> restrictedTLS() } }.newRequest().apply { + if (DataStore.allowInsecureOnRequest) { + allowInsecure() + } setURL(subscription.link) setUserAgent(subscription.customUserAgent.takeIf { it.isNotBlank() } ?: USER_AGENT) }.execute() - - proxies = parseRaw(response.contentString) + proxies = parseRaw(Util.getStringBox(response.contentString)) ?: error(app.getString(R.string.no_proxies_found)) - subscription.subscriptionUserinfo = response.getHeader("Subscription-Userinfo") + subscription.subscriptionUserinfo = + Util.getStringBox(response.getHeader("Subscription-Userinfo")) + + // 修改默认名字 + if (proxyGroup.name?.startsWith("Subscription #") == true) { + var remoteName = Util.getStringBox(response.getHeader("content-disposition")) + if (remoteName.isNotBlank()) { + remoteName = Util.decodeFilename(remoteName) + if (remoteName.isNotBlank()) { + proxyGroup.name = remoteName + } + } + } } val proxiesMap = LinkedHashMap() @@ -167,11 +186,12 @@ object RawUpdater : GroupUpdater() { } } else { changed++ - SagerDatabase.proxyDao.addProxy(ProxyEntity( - groupId = proxyGroup.id, userOrder = userOrder - ).apply { - putBean(bean) - }) + SagerDatabase.proxyDao.addProxy( + ProxyEntity( + groupId = proxyGroup.id, userOrder = userOrder + ).apply { + putBean(bean) + }) added.add(name) Logs.d("Inserted profile: $name") } @@ -208,12 +228,17 @@ object RawUpdater : GroupUpdater() { if (text.contains("proxies:")) { + // clash & meta + try { - // clash - for (proxy in (Yaml().apply { + val yaml = Yaml().apply { addTypeDescription(TypeDescription(String::class.java, "str")) - }.loadAs(text, Map::class.java)["proxies"] as? (List>) ?: error( + }.loadAs(text, Map::class.java) + + val globalClientFingerprint = yaml["global-client-fingerprint"]?.toString() ?: "" + + for (proxy in (yaml["proxies"] as? (List>) ?: error( app.getString(R.string.no_proxies_found_in_file) ))) { // Note: YAML numbers parsed as "Long" @@ -238,6 +263,7 @@ object RawUpdater : GroupUpdater() { setTLS(proxy["tls"]?.toString() == "true") sni = proxy["sni"]?.toString() name = proxy["name"]?.toString() + allowInsecure = proxy["skip-cert-verify"]?.toString() == "true" }) } @@ -276,201 +302,365 @@ object RawUpdater : GroupUpdater() { }) } - "vmess", "vless" -> { - val isVLESS = proxy["type"].toString() == "vless" - val bean = VMessBean().apply { if (isVLESS) alterId = -1 } + "vmess", "vless", "trojan" -> { + val bean = when (proxy["type"] as String) { + "vmess" -> VMessBean() + "vless" -> VMessBean().apply { + alterId = -1 // make it VLESS + packetEncoding = 2 // clash meta default XUDP + } + + "trojan" -> TrojanBean().apply { + security = "tls" + } + + else -> error("impossible") + } + + bean.serverAddress = proxy["server"]?.toString() ?: continue + bean.serverPort = proxy["port"]?.toString()?.toIntOrNull() ?: continue + for (opt in proxy) { when (opt.key) { "name" -> bean.name = opt.value?.toString() - "server" -> bean.serverAddress = opt.value as String - "port" -> bean.serverPort = opt.value.toString().toInt() - "uuid" -> bean.uuid = opt.value as String + "password" -> if (bean is TrojanBean) bean.password = + opt.value?.toString() - "alterId" -> if (!isVLESS) bean.alterId = - opt.value.toString().toInt() + "uuid" -> if (bean is VMessBean) bean.uuid = + opt.value?.toString() - "cipher" -> if (!isVLESS) bean.encryption = opt.value as String + "alterId" -> if (bean is VMessBean && !bean.isVLESS) bean.alterId = + opt.value?.toString()?.toIntOrNull() - "flow" -> if (isVLESS) bean.encryption = opt.value as String + "cipher" -> if (bean is VMessBean && !bean.isVLESS) bean.encryption = + (opt.value as? String) - "network" -> { - bean.type = opt.value as String - // Clash "network" fix - when (bean.type) { - "h2" -> bean.type = "http" + "flow" -> if (bean is VMessBean && bean.isVLESS) { + (opt.value as? String)?.let { + if (it.contains("xtls-rprx-vision")) { + bean.encryption = "xtls-rprx-vision" + } } } + "packet-encoding" -> if (bean is VMessBean) { + bean.packetEncoding = when ((opt.value as? String)) { + "packetaddr" -> 1 + "xudp" -> 2 + else -> 0 + } + } + + "tls" -> if (bean is VMessBean) { + bean.security = + if (opt.value as? Boolean == true) "tls" else "" + } + + "servername", "sni" -> bean.sni = opt.value?.toString() + + "alpn" -> bean.alpn = + (opt.value as? List)?.joinToString("\n") + + "skip-cert-verify" -> bean.allowInsecure = + opt.value as? Boolean == true + "client-fingerprint" -> bean.utlsFingerprint = opt.value as String - "tls" -> bean.security = - if (opt.value?.toString() == "true") "tls" else "" + "reality-opts" -> (opt.value as? Map)?.also { + for (realityOpt in it) { + bean.security = "tls" - "skip-cert-verify" -> bean.allowInsecure = - opt.value?.toString() == "true" + when (realityOpt.key) { + "public-key" -> bean.realityPubKey = + realityOpt.value?.toString() + + "short-id" -> bean.realityShortId = + realityOpt.value?.toString() + } + } + } - "ws-path" -> bean.path = opt.value?.toString() - "ws-headers" -> for (wsHeader in (opt.value as Map)) { - when (wsHeader.key.lowercase()) { - "host" -> bean.host = wsHeader.value.toString() + "network" -> { + when (opt.value) { + "h2", "http" -> bean.type = "http" + "ws", "grpc" -> bean.type = opt.value as String } } - "ws-opts", "ws-opt" -> for (wsOpt in (opt.value as Map)) { - when (wsOpt.key.lowercase()) { - "headers" -> for (wsHeader in (opt.value as Map)) { - when (wsHeader.key.lowercase()) { - "host" -> bean.host = wsHeader.value.toString() + "ws-opts" -> (opt.value as? Map)?.also { + for (wsOpt in it) { + when (wsOpt.key) { + "headers" -> (wsOpt.value as? Map)?.forEach { (key, value) -> + when (key.toString().lowercase()) { + "host" -> { + bean.host = value?.toString() + } + } } - } - "path" -> { - bean.path = wsOpt.value.toString() - } + "path" -> { + bean.path = wsOpt.value?.toString() + } - "max-early-data" -> { - bean.wsMaxEarlyData = wsOpt.value.toString().toInt() - } + "max-early-data" -> { + bean.wsMaxEarlyData = + wsOpt.value?.toString()?.toIntOrNull() + } + + "early-data-header-name" -> { + bean.earlyDataHeaderName = + wsOpt.value?.toString() + } - "early-data-header-name" -> { - bean.earlyDataHeaderName = wsOpt.value.toString() + "v2ray-http-upgrade" -> { + if (wsOpt.value as? Boolean == true) { + bean.type = "httpupgrade" + } + } } } } - "servername" -> bean.host = opt.value?.toString() - // The format of the VMessBean is wrong, so the `host` `path` has some strange transformations here. - "h2-opts", "h2-opt" -> for (h2Opt in (opt.value as Map)) { - when (h2Opt.key.lowercase()) { - "host" -> bean.host = - (h2Opt.value as List).first() + "h2-opts" -> (opt.value as? Map)?.also { + for (h2Opt in it) { + when (h2Opt.key) { + "host" -> bean.host = + (h2Opt.value as? List)?.joinToString("\n") - "path" -> bean.path = h2Opt.value.toString() + "path" -> bean.path = h2Opt.value?.toString() + } } } - "http-opts", "http-opt" -> for (httpOpt in (opt.value as Map)) { - when (httpOpt.key.lowercase()) { - "path" -> bean.path = - (httpOpt.value as List).first() - - "headers" -> for (hdr in (httpOpt.value as Map)) { - when (hdr.key.lowercase()) { - "host" -> bean.host = - (hdr.value as List).first() + "http-opts" -> (opt.value as? Map)?.also { + for (httpOpt in it) { + when (httpOpt.key) { + "path" -> bean.path = + (httpOpt.value as? List)?.joinToString("\n") + + "headers" -> { + (httpOpt.value as? Map>)?.forEach { (key, value) -> + when (key.toString().lowercase()) { + "host" -> { + bean.host = value.joinToString("\n") + } + } + } } } } } - "grpc-opts", "grpc-opt" -> for (grpcOpt in (opt.value as Map)) { - when (grpcOpt.key.lowercase()) { - "grpc-service-name" -> bean.path = - grpcOpt.value.toString() + "grpc-opts" -> (opt.value as? Map)?.also { + for (grpcOpt in it) { + when (grpcOpt.key) { + "grpc-service-name" -> bean.path = + grpcOpt.value?.toString() + } } } - "reality-opts" -> for (realityOpt in (opt.value as Map)) { - when (realityOpt.key.lowercase()) { - "public-key" -> bean.realityPubKey = - realityOpt.value.toString() + "smux" -> (opt.value as? Map)?.also { + for (smuxOpt in it) { + when (smuxOpt.key) { + "enabled" -> bean.enableMux = + smuxOpt.value.toString() == "true" + + "max-streams" -> bean.muxConcurrency = + smuxOpt.value.toString().toInt() - "short-id" -> bean.realityShortId = - realityOpt.value.toString() + "padding" -> bean.muxPadding = + smuxOpt.value.toString() == "true" + } } } } } - if (bean.isTLS() && bean.sni.isNullOrBlank() && !bean.host.isNullOrBlank()) { - bean.sni = bean.host - } proxies.add(bean) } - "trojan" -> { - val bean = TrojanBean() - bean.security = "tls" + "anytls" -> { + val bean = AnyTLSBean() for (opt in proxy) { - when (opt.key) { - "name" -> bean.name = opt.value?.toString() + if (opt.value == null) continue + when (opt.key.replace("_", "-")) { + "name" -> bean.name = opt.value.toString() "server" -> bean.serverAddress = opt.value as String "port" -> bean.serverPort = opt.value.toString().toInt() - "password" -> bean.password = opt.value?.toString() + "password" -> bean.password = opt.value.toString() "client-fingerprint" -> bean.utlsFingerprint = opt.value as String - "sni" -> bean.sni = opt.value?.toString() + "sni" -> bean.sni = opt.value.toString() "skip-cert-verify" -> bean.allowInsecure = - opt.value?.toString() == "true" + opt.value.toString() == "true" - "network" -> when (opt.value) { - "ws", "grpc" -> bean.type = opt.value?.toString() + "alpn" -> { + val alpn = (opt.value as? (List)) + bean.alpn = alpn?.joinToString("\n") } + } + } + proxies.add(bean) + } - "ws-opts", "ws-opt" -> for (wsOpt in (opt.value as Map)) { - when (wsOpt.key.lowercase()) { - "headers" -> for (wsHeader in (opt.value as Map)) { - when (wsHeader.key.lowercase()) { - "host" -> bean.host = wsHeader.value.toString() - } - } + "hysteria" -> { + val bean = HysteriaBean() + bean.protocolVersion = 1 + var hopPorts = "" + for (opt in proxy) { + if (opt.value == null) continue + when (opt.key.replace("_", "-")) { + "name" -> bean.name = opt.value.toString() + "server" -> bean.serverAddress = opt.value as String + "port" -> bean.serverPorts = opt.value.toString() + "ports" -> hopPorts = opt.value.toString() - "path" -> { - bean.path = wsOpt.value.toString() - } - } + "obfs" -> bean.obfuscation = opt.value.toString() + + "auth-str" -> { + bean.authPayloadType = HysteriaBean.TYPE_STRING + bean.authPayload = opt.value.toString() } - "grpc-opts", "grpc-opt" -> for (grpcOpt in (opt.value as Map)) { - when (grpcOpt.key.lowercase()) { - "grpc-service-name" -> bean.path = - grpcOpt.value.toString() - } + "sni" -> bean.sni = opt.value.toString() + + "skip-cert-verify" -> bean.allowInsecure = + opt.value.toString() == "true" + + "up" -> bean.uploadMbps = + opt.value.toString().substringBefore(" ").toIntOrNull() + ?: 100 + + "down" -> bean.downloadMbps = + opt.value.toString().substringBefore(" ").toIntOrNull() + ?: 100 + + "recv-window-conn" -> bean.connectionReceiveWindow = + opt.value.toString().toIntOrNull() ?: 0 + + "recv-window" -> bean.streamReceiveWindow = + opt.value.toString().toIntOrNull() ?: 0 + + "disable-mtu-discovery" -> bean.disableMtuDiscovery = + opt.value.toString() == "true" || opt.value.toString() == "1" + + "alpn" -> { + val alpn = (opt.value as? (List)) + bean.alpn = alpn?.joinToString("\n") ?: "h3" } } } + if (hopPorts.isNotBlank()) { + bean.serverPorts = hopPorts + } proxies.add(bean) } - "hysteria" -> { + "hysteria2" -> { val bean = HysteriaBean() + bean.protocolVersion = 2 + var hopPorts = "" for (opt in proxy) { - when (opt.key) { - "name" -> bean.name = opt.value?.toString() + if (opt.value == null) continue + when (opt.key.replace("_", "-")) { + "name" -> bean.name = opt.value.toString() "server" -> bean.serverAddress = opt.value as String - "port" -> bean.serverPort = opt.value.toString().toInt() - "auth_str" -> { - bean.authPayloadType = HysteriaBean.TYPE_STRING - bean.authPayload = opt.value?.toString() - } + "port" -> bean.serverPorts = opt.value.toString() + "ports" -> hopPorts = opt.value.toString() - "sni" -> bean.sni = opt.value?.toString() + "obfs-password" -> bean.obfuscation = opt.value.toString() + + "password" -> bean.authPayload = opt.value.toString() + + "sni" -> bean.sni = opt.value.toString() "skip-cert-verify" -> bean.allowInsecure = - opt.value?.toString() == "true" + opt.value.toString() == "true" "up" -> bean.uploadMbps = - opt.value?.toString()?.toIntOrNull() ?: 100 + opt.value.toString().substringBefore(" ").toIntOrNull() ?: 0 "down" -> bean.downloadMbps = - opt.value?.toString()?.toIntOrNull() ?: 100 + opt.value.toString().substringBefore(" ").toIntOrNull() ?: 0 + } + } + if (hopPorts.isNotBlank()) { + bean.serverPorts = hopPorts + } + proxies.add(bean) + } + + "tuic" -> { + val bean = TuicBean() + var ip = "" + for (opt in proxy) { + if (opt.value == null) continue + when (opt.key.replace("_", "-")) { + "name" -> bean.name = opt.value.toString() + "server" -> bean.serverAddress = opt.value.toString() + "ip" -> ip = opt.value.toString() + "port" -> bean.serverPort = opt.value.toString().toInt() + + "token" -> { + bean.protocolVersion = 4 + bean.token = opt.value.toString() + } + + "uuid" -> bean.uuid = opt.value.toString() + + "password" -> bean.token = opt.value.toString() + + "skip-cert-verify" -> bean.allowInsecure = + opt.value.toString() == "true" + + "disable-sni" -> bean.disableSNI = + opt.value.toString() == "true" - "disable_mtu_discovery" -> bean.disableMtuDiscovery = - opt.value?.toString() == "true" || opt.value?.toString() == "1" + "reduce-rtt" -> bean.reduceRTT = + opt.value.toString() == "true" + + "sni" -> bean.sni = opt.value.toString() "alpn" -> { val alpn = (opt.value as? (List)) - bean.alpn = alpn?.joinToString("\n") ?: "h3" + bean.alpn = alpn?.joinToString("\n") } + "congestion-controller" -> bean.congestionController = + opt.value.toString() + + "udp-relay-mode" -> bean.udpRelayMode = opt.value.toString() + + } + } + if (ip.isNotBlank()) { + bean.serverAddress = ip + if (bean.sni.isNullOrBlank() && !bean.serverAddress.isNullOrBlank() && !bean.serverAddress.isIpAddress()) { + bean.sni = bean.serverAddress } } proxies.add(bean) } } } - proxies.forEach { it.initializeDefaultValues() } + + // Fix ent + proxies.forEach { + it.initializeDefaultValues() + if (it is StandardV2RayBean) { + // 1. SNI + if (it.isTLS() && it.sni.isNullOrBlank() && !it.host.isNullOrBlank() && !it.host.isIpAddress()) { + it.sni = it.host + } + // 2. globalClientFingerprint + if (!it.realityPubKey.isNullOrBlank() && it.utlsFingerprint.isNullOrBlank()) { + it.utlsFingerprint = globalClientFingerprint + if (it.utlsFingerprint.isNullOrBlank()) it.utlsFingerprint = "chrome" + } + } + } return proxies } catch (e: YAMLException) { Logs.w(e) @@ -526,6 +716,7 @@ object RawUpdater : GroupUpdater() { if (localAddresses.isNullOrEmpty()) error("Empty address in 'Interface' selection") bean.localAddress = localAddresses.flatMap { it.split(",") }.joinToString("\n") bean.privateKey = iface["PrivateKey"] + bean.mtu = iface["MTU"]?.toIntOrNull() val peers = ini.getAll("Peer") if (peers.isNullOrEmpty()) error("Missing 'Peer' selections") val beans = mutableListOf() @@ -552,7 +743,7 @@ object RawUpdater : GroupUpdater() { if (json is JSONObject) { when { json.has("server") && (json.has("up") || json.has("up_mbps")) -> { - return listOf(json.parseHysteria()) + return listOf(json.parseHysteria1Json()) } json.has("method") -> { @@ -564,9 +755,25 @@ object RawUpdater : GroupUpdater() { } json.has("outbounds") -> { - return listOf(ConfigBean().applyDefaultValues().apply { - config = json.toStringPretty() - }) + return json.getJSONArray("outbounds") + .filterIsInstance() + .mapNotNull { + val ty = it.getStr("type") + if (ty == null || ty == "" || + ty == "dns" || ty == "block" || ty == "direct" || ty == "selector" || ty == "urltest" + ) { + null + } else { + it + } + }.map { + ConfigBean().apply { + applyDefaultValues() + type = 1 + config = it.toStringPretty() + name = it.getStr("tag") + } + } } json.has("server") && json.has("server_port") -> { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt index af3c96cc7..6c6696408 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt @@ -2,6 +2,8 @@ package io.nekohasekai.sagernet.ktx +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.* fun block(block: suspend CoroutineScope.() -> Unit): suspend CoroutineScope.() -> Unit { @@ -11,6 +13,9 @@ fun block(block: suspend CoroutineScope.() -> Unit): suspend CoroutineScope.() - fun runOnDefaultDispatcher(block: suspend CoroutineScope.() -> Unit) = GlobalScope.launch(Dispatchers.Default, block = block) +fun Fragment.runOnLifecycleDispatcher(block: suspend CoroutineScope.() -> Unit) = + lifecycleScope.launch(Dispatchers.Default, block = block) + suspend fun onDefaultDispatcher(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Default, block = block) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt index 53a5f69e1..3dbc532d5 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt @@ -1,5 +1,6 @@ package io.nekohasekai.sagernet.ktx +import android.app.Activity import android.content.Context import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment @@ -13,4 +14,15 @@ fun Context.alert(text: String): AlertDialog { .create() } -fun Fragment.alert(text: String) = requireContext().alert(text) \ No newline at end of file +fun Fragment.alert(text: String) = requireContext().alert(text) + +fun AlertDialog.tryToShow() { + try { + val activity = context as Activity + if (!activity.isFinishing) { + show() + } + } catch (e: Exception) { + Logs.e(e) + } +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt index aa28fb8e8..6c0f8f45b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt @@ -4,19 +4,20 @@ import com.google.gson.JsonParser import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.Serializable import io.nekohasekai.sagernet.fmt.http.parseHttp -import io.nekohasekai.sagernet.fmt.hysteria.parseHysteria +import io.nekohasekai.sagernet.fmt.hysteria.parseHysteria1 +import io.nekohasekai.sagernet.fmt.hysteria.parseHysteria2 import io.nekohasekai.sagernet.fmt.naive.parseNaive import io.nekohasekai.sagernet.fmt.parseUniversal import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks import io.nekohasekai.sagernet.fmt.socks.parseSOCKS import io.nekohasekai.sagernet.fmt.trojan.parseTrojan +import io.nekohasekai.sagernet.fmt.tuic.parseTuic import io.nekohasekai.sagernet.fmt.trojan_go.parseTrojanGo import io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray -import moe.matsuri.nb4a.plugin.NekoPluginManager -import moe.matsuri.nb4a.proxy.neko.NekoJSInterface -import moe.matsuri.nb4a.proxy.neko.parseShareLink +import moe.matsuri.nb4a.proxy.anytls.parseAnytls import moe.matsuri.nb4a.utils.JavaUtil.gson import moe.matsuri.nb4a.utils.Util +import okhttp3.HttpUrl import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -109,7 +110,7 @@ suspend fun parseProxies(text: String): List { val entities = ArrayList() val entitiesByLine = ArrayList() - suspend fun String.parseLink(entities: ArrayList) { + fun String.parseLink(entities: ArrayList) { if (startsWith("clash://install-config?") || startsWith("sn://subscription?")) { throw SubscriptionFoundException(this) } @@ -137,6 +138,14 @@ suspend fun parseProxies(text: String): List { entities.add(parseHttp(this)) }.onFailure { Logs.w(it) + val clashUrl = HttpUrl.Builder() + .scheme("https") + .host("install-config") + .addQueryParameter("url", this) + .build() + .toString() + .replaceFirst("https://", "clash://") + throw (SubscriptionFoundException(clashUrl)) } } else if (startsWith("vmess://")) { Logs.d("Try parse v2ray link: $this") @@ -181,27 +190,32 @@ suspend fun parseProxies(text: String): List { Logs.w(it) } } else if (startsWith("hysteria://")) { - Logs.d("Try parse hysteria link: $this") + Logs.d("Try parse hysteria1 link: $this") runCatching { - entities.add(parseHysteria(this)) + entities.add(parseHysteria1(this)) }.onFailure { Logs.w(it) } - } else { // Neko Plugins - NekoPluginManager.getProtocols().forEach { obj -> - obj.protocolConfig.optJSONArray("links")?.forEach { _, any -> - if (any is String && startsWith(any)) { - runCatching { - entities.add( - parseShareLink( - obj.plgId, obj.protocolId, this@parseLink - ) - ) - }.onFailure { - Logs.w(it) - } - } - } + } else if (startsWith("hysteria2://") || startsWith("hy2://")) { + Logs.d("Try parse hysteria2 link: $this") + runCatching { + entities.add(parseHysteria2(this)) + }.onFailure { + Logs.w(it) + } + } else if (startsWith("tuic://")) { + Logs.d("Try parse TUIC link: $this") + runCatching { + entities.add(parseTuic(this)) + }.onFailure { + Logs.w(it) + } + } else if (startsWith("anytls://")) { + Logs.d("Try parse anytls link: $this") + runCatching { + entities.add(parseAnytls(this)) + }.onFailure { + Logs.w(it) } } } @@ -212,17 +226,16 @@ suspend fun parseProxies(text: String): List { for (link in linksByLine) { link.parseLink(entitiesByLine) } - var isBadLink = false +// var isBadLink = false if (entities.onEach { it.initializeDefaultValues() }.size == entitiesByLine.onEach { it.initializeDefaultValues() }.size) run test@{ entities.forEachIndexed { index, bean -> val lineBean = entitiesByLine[index] if (bean == lineBean && bean.displayName() != lineBean.displayName()) { - isBadLink = true +// isBadLink = true return@test } } } - NekoJSInterface.Default.destroyAllJsi() return if (entities.size > entitiesByLine.size) entities else entitiesByLine } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt index d8ff48026..86b718733 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt @@ -2,6 +2,7 @@ package io.nekohasekai.sagernet.ktx +import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.fmt.AbstractBean import moe.matsuri.nb4a.utils.NGUtil import okhttp3.HttpUrl @@ -62,4 +63,4 @@ fun mkPort(): Int { return port } -const val USER_AGENT = "NekoBox/Android/1.0 (Prefer ClashMeta Format)" +const val USER_AGENT = "NekoBox/Android/" + BuildConfig.VERSION_NAME + " (Prefer ClashMeta Format)" diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt index d0ce4d81b..3e176c8ed 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt @@ -5,8 +5,11 @@ package io.nekohasekai.sagernet.ktx import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint -import android.content.* -import android.content.pm.PackageInfo +import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.res.Resources import android.os.Build import android.system.Os @@ -17,6 +20,8 @@ import androidx.activity.result.ActivityResultLauncher import androidx.annotation.AttrRes import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -27,16 +32,23 @@ import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet -import io.nekohasekai.sagernet.bg.Executable +import io.nekohasekai.sagernet.aidl.ISagerNetService +import io.nekohasekai.sagernet.bg.BaseService +import io.nekohasekai.sagernet.bg.SagerConnection import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.database.SagerDatabase -import io.nekohasekai.sagernet.database.preference.PublicDatabase import io.nekohasekai.sagernet.ui.MainActivity import io.nekohasekai.sagernet.ui.ThemedActivity -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import moe.matsuri.nb4a.utils.NGUtil import java.io.FileDescriptor -import java.net.* +import java.net.HttpURLConnection +import java.net.InetAddress +import java.net.Socket +import java.net.URLEncoder import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -48,6 +60,7 @@ import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KProperty import kotlin.reflect.KProperty0 +fun String?.blankAsNull(): String? = if (isNullOrBlank()) null else this inline fun Iterable.forEachTry(action: (T) -> Unit) { var result: Exception? = null @@ -113,9 +126,6 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni }) } -val PackageInfo.signaturesCompat - get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures - /** * Based on: https://stackoverflow.com/a/26348729/2245107 */ @@ -193,7 +203,7 @@ val shortAnimTime by lazy { fun View.crossFadeFrom(other: View) { clearAnimation() other.clearAnimation() - if (visibility == View.VISIBLE && other.visibility == View.GONE) return + if (isVisible && other.isGone) return alpha = 0F visibility = View.VISIBLE animate().alpha(1F).duration = shortAnimTime @@ -240,16 +250,33 @@ fun Fragment.needReload() { fun Fragment.needRestart() { snackbar(R.string.need_restart).setAction(R.string.apply) { + triggerFullRestart(requireContext()) + }.show() +} + +fun triggerFullRestart(ctx: Context) { + runOnDefaultDispatcher { SagerNet.stopService() - val ctx = requireContext() - runOnDefaultDispatcher { - delay(500) - SagerDatabase.instance.close() - PublicDatabase.instance.close() - Executable.killAll(true) + delay(500) + SagerConnection.restartingApp = true + val connection = SagerConnection(SagerConnection.CONNECTION_ID_RESTART_BG) + connection.connect(ctx, RestartCallback { ProcessPhoenix.triggerRebirth(ctx, Intent(ctx, MainActivity::class.java)) - } - }.show() + }) + } +} + +private class RestartCallback(val callback: () -> Unit) : SagerConnection.Callback { + override fun stateChanged( + state: BaseService.State, + profileName: String?, + msg: String? + ) { + } + + override fun onServiceConnected(service: ISagerNetService) { + callback() + } } fun Context.getColour(@ColorRes colorRes: Int): Int { @@ -262,13 +289,10 @@ fun Context.getColorAttr(@AttrRes resId: Int): Int { }.resourceId) } -var isExpert: Boolean - get() = BuildConfig.DEBUG || DataStore.isExpert - set(value) = TODO() - -val isExpertFlavor = ((BuildConfig.FLAVOR == "expert") || BuildConfig.DEBUG) +val isExpert: Boolean by lazy { BuildConfig.DEBUG || DataStore.isExpert } const val isOss = BuildConfig.FLAVOR == "oss" -const val isFdroid = BuildConfig.FLAVOR == "fdroid" +const val isPlay = BuildConfig.FLAVOR == "play" +const val isPreview = BuildConfig.FLAVOR == "preview" fun Continuation.tryResume(value: T) { try { diff --git a/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt b/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt index 76fd704d6..10307a1f7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt @@ -32,7 +32,8 @@ object PluginManager { val result = initNative(pluginId) if (result != null) return result } catch (t: Throwable) { - if (throwable == null) throwable = t else Logs.w(t) + throwable = t + Logs.w(t) } throw throwable ?: PluginNotFoundException(pluginId) @@ -41,16 +42,41 @@ object PluginManager { private fun initNative(pluginId: String): InitResult? { val info = Plugins.getPlugin(pluginId) ?: return null + // internal so + if (info.applicationInfo == null) { + try { + initNativeInternal(pluginId)?.let { return InitResult(it, info) } + } catch (t: Throwable) { + Logs.w("initNativeInternal failed", t) + } + return null + } + try { - initNativeFaster(info)?.also { return InitResult(it, info) } + initNativeFaster(info)?.let { return InitResult(it, info) } } catch (t: Throwable) { - Logs.w("Initializing native plugin faster mode failed", t) + Logs.w("initNativeFaster failed", t) } Logs.w("Init native returns empty result") return null } + private fun initNativeInternal(pluginId: String): String? { + fun soIfExist(soName: String): String? { + val f = File(SagerNet.application.applicationInfo.nativeLibraryDir, soName) + if (f.canExecute()) { + return f.absolutePath + } + return null + } + return when (pluginId) { + "hysteria-plugin" -> soIfExist("libhysteria.so") + "hysteria2-plugin" -> soIfExist("libhysteria2.so") + else -> null + } + } + private fun initNativeFaster(provider: ProviderInfo): String? { return provider.loadString(Plugins.METADATA_KEY_EXECUTABLE_PATH) ?.let { relativePath -> @@ -64,6 +90,7 @@ object PluginManager { is String -> value is Int -> SagerNet.application.packageManager.getResourcesForApplication(applicationInfo) .getString(value) + null -> null else -> error("meta-data $key has invalid type ${value.javaClass}") } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt index 70760dfb8..f6afc4b3f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt @@ -10,6 +10,7 @@ import android.os.PowerManager import android.provider.Settings import android.text.util.Linkify import android.view.View +import android.widget.Toast import androidx.activity.result.component1 import androidx.activity.result.component2 import androidx.activity.result.contract.ActivityResultContracts @@ -25,9 +26,15 @@ import io.nekohasekai.sagernet.databinding.LayoutAboutBinding import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.plugin.PluginManager.loadString import io.nekohasekai.sagernet.utils.PackageCache -import io.nekohasekai.sagernet.widget.ListHolderListener +import io.nekohasekai.sagernet.widget.ListListener import libcore.Libcore import moe.matsuri.nb4a.plugin.Plugins +import androidx.core.net.toUri +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.database.DataStore +import moe.matsuri.nb4a.utils.Util +import org.json.JSONObject class AboutFragment : ToolbarFragment(R.layout.layout_about) { @@ -36,7 +43,7 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) { val binding = LayoutAboutBinding.bind(view) - ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener) + ViewCompat.setOnApplyWindowInsetsListener(view, ListListener) toolbar.setTitle(R.string.menu_about) parentFragmentManager.beginTransaction() @@ -65,117 +72,133 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) { } override fun getMaterialAboutList(activityContext: Context): MaterialAboutList { - - var versionName = BuildConfig.VERSION_NAME - if (!isOss) { - versionName += " ${BuildConfig.FLAVOR}" - } - if (BuildConfig.DEBUG) { - versionName += " DEBUG" - } - return MaterialAboutList.Builder() - .addCard(MaterialAboutCard.Builder() - .outline(false) - .addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_update_24) - .text(R.string.app_version) - .subText(versionName) - .setOnClickAction { - requireContext().launchCustomTab( - "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases" - ) - } - .build()) - .addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_layers_24) - .text(getString(R.string.version_x, "sing-box")) - .subText(Libcore.versionBox()) - .setOnClickAction { } - .build()) - .addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_card_giftcard_24) - .text(R.string.donate) - .subText(R.string.donate_info) - .setOnClickAction { - requireContext().launchCustomTab( - "https://matsuridayo.github.io/#donate" - ) - } - .build()) - .apply { - for ((_, pkg) in PackageCache.installedPluginPackages) { - try { - val pluginId = pkg.providers[0].loadString(Plugins.METADATA_KEY_ID) - if (pluginId.isNullOrBlank() || pluginId.startsWith(Plugins.AUTHORITIES_PREFIX_NEKO_PLUGIN)) continue - addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_nfc_24) - .text( - getString( - R.string.version_x, - pluginId - ) + " (${Plugins.displayExeProvider(pkg.packageName)})" + .addCard( + MaterialAboutCard.Builder() + .outline(false) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_update_24) + .text(R.string.app_version) + .subText(SagerNet.appVersionNameForDisplay) + .setOnClickAction { + requireContext().launchCustomTab( + "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases" + ) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .text(R.string.check_update_release) + .setOnClickAction { + checkUpdate(false) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .text(R.string.check_update_preview) + .setOnClickAction { + checkUpdate(true) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_layers_24) + .text(getString(R.string.version_x, "sing-box")) + .subText(Libcore.versionBox()) + .setOnClickAction { } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_card_giftcard_24) + .text(R.string.donate) + .subText(R.string.donate_info) + .setOnClickAction { + requireContext().launchCustomTab( + "https://matsuridayo.github.io/index_docs/#donate" ) - .subText("v" + pkg.versionName) - .setOnClickAction { - startActivity(Intent().apply { - action = - Settings.ACTION_APPLICATION_DETAILS_SETTINGS - data = Uri.fromParts( - "package", pkg.packageName, null + } + .build()) + .apply { + PackageCache.awaitLoadSync() + for ((_, pkg) in PackageCache.installedPluginPackages) { + try { + val pluginId = + pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID) + if (pluginId.isNullOrBlank()) continue + addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_nfc_24) + .text( + getString( + R.string.version_x, + pluginId + ) + " (${Plugins.displayExeProvider(pkg.packageName)})" ) - }) - } - .build()) - } catch (e: Exception) { - Logs.w(e) + .subText("v" + pkg.versionName) + .setOnClickAction { + startActivity(Intent().apply { + action = + Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts( + "package", pkg.packageName, null + ) + }) + } + .build()) + } catch (e: Exception) { + Logs.w(e) + } } } - } - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager - if (!pm.isIgnoringBatteryOptimizations(app.packageName)) { - addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_running_with_errors_24) - .text(R.string.ignore_battery_optimizations) - .subText(R.string.ignore_battery_optimizations_sum) - .setOnClickAction { - requestIgnoreBatteryOptimizations.launch( - Intent( - Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:${app.packageName}") - ) - ) - } - .build()) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager + if (!pm.isIgnoringBatteryOptimizations(app.packageName)) { + addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_running_with_errors_24) + .text(R.string.ignore_battery_optimizations) + .subText(R.string.ignore_battery_optimizations_sum) + .setOnClickAction { + requestIgnoreBatteryOptimizations.launch( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "package:${app.packageName}".toUri() + ) + ) + } + .build()) + } } } - } - .build()) - .addCard(MaterialAboutCard.Builder() - .outline(false) - .title(R.string.project) - .addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_sanitizer_24) - .text(R.string.github) - .setOnClickAction { - requireContext().launchCustomTab( - "https://github.com/MatsuriDayo/NekoBoxForAndroid" - - ) - } .build()) - .addItem(MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_qu_shadowsocks_foreground) - .text(R.string.telegram) - .setOnClickAction { - requireContext().launchCustomTab( - "https://t.me/MatsuriDayo" - ) - } + .addCard( + MaterialAboutCard.Builder() + .outline(false) + .title(R.string.project) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_sanitizer_24) + .text(R.string.github) + .setOnClickAction { + requireContext().launchCustomTab( + "https://github.com/MatsuriDayo/NekoBoxForAndroid" + + ) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_qu_shadowsocks_foreground) + .text(R.string.telegram) + .setOnClickAction { + requireContext().launchCustomTab( + "https://t.me/MatsuriDayo" + ) + } + .build()) .build()) - .build()) .build() } @@ -188,6 +211,69 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) { } } + fun checkUpdate(checkPreview: Boolean) { + runOnIoDispatcher { + try { + val client = Libcore.newHttpClient().apply { + modernTLS() + trySocks5(DataStore.mixedPort) + } + val response = client.newRequest().apply { + if (checkPreview) { + setURL("https://api.github.com/repos/MatsuriDayo/NekoBoxForAndroid/releases/tags/preview") + } else { + setURL("https://api.github.com/repos/MatsuriDayo/NekoBoxForAndroid/releases/latest") + } + }.execute() + val release = JSONObject(Util.getStringBox(response.contentString)) + val releaseName = release.getString("name") + val releaseUrl = release.getString("html_url") + var haveUpdate = releaseName.isNotBlank() + haveUpdate = if (isPreview) { + if (checkPreview) { + haveUpdate && releaseName != BuildConfig.PRE_VERSION_NAME + } else { + // User: 1.3.9 pre-1.4.0 Stable: 1.3.9 -> No update + haveUpdate && releaseName != BuildConfig.VERSION_NAME + } + } else { + // User: 1.4.0 Preview: pre-1.4.0 -> No update + // User: 1.4.0 Preview: pre-1.4.1 -> Update + // User: 1.4.0 Stable: 1.4.0 -> No update + // User: 1.4.0 Stable: 1.4.1 -> Update + haveUpdate && !releaseName.contains(BuildConfig.VERSION_NAME) + } + runOnMainDispatcher { + if (haveUpdate) { + val context = requireContext() + MaterialAlertDialogBuilder(context) + .setTitle(R.string.update_dialog_title) + .setMessage( + context.getString( + R.string.update_dialog_message, + SagerNet.appVersionNameForDisplay, + releaseName + ) + ) + .setPositiveButton(R.string.yes) { _, _ -> + val intent = Intent(Intent.ACTION_VIEW, releaseUrl.toUri()) + context.startActivity(intent) + } + .setNegativeButton(R.string.no, null) + .show() + } else { + Toast.makeText(app, R.string.check_update_no, Toast.LENGTH_SHORT).show() + } + } + } catch (e: Exception) { + Logs.w(e) + runOnMainDispatcher { + Toast.makeText(app, e.readableMessage, Toast.LENGTH_SHORT).show() + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt index 3c522fabb..7f3892430 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt @@ -2,20 +2,21 @@ package io.nekohasekai.sagernet.ui import android.content.Intent import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.Bundle import android.util.SparseBooleanArray -import android.view.* +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.Filter import android.widget.Filterable import androidx.annotation.UiThread import androidx.core.util.contains import androidx.core.util.set import androidx.core.view.ViewCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DefaultItemAnimator @@ -24,29 +25,30 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import io.nekohasekai.sagernet.BuildConfig -import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.databinding.LayoutAppListBinding import io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding -import io.nekohasekai.sagernet.ktx.* +import io.nekohasekai.sagernet.ktx.crossFadeFrom +import io.nekohasekai.sagernet.ktx.onMainDispatcher +import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.utils.PackageCache -import io.nekohasekai.sagernet.widget.ListHolderListener import io.nekohasekai.sagernet.widget.ListListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext -import moe.matsuri.nb4a.plugin.NekoPluginManager -import moe.matsuri.nb4a.plugin.Plugins -import moe.matsuri.nb4a.proxy.neko.NekoJSInterface -import moe.matsuri.nb4a.ui.Dialogs import kotlin.coroutines.coroutineContext class AppListActivity : ThemedActivity() { companion object { private const val SWITCH = "switch" + + private val cachedApps + get() = PackageCache.installedPackages.toMutableMap().apply { + remove(BuildConfig.APPLICATION_ID) + } } private class ProxiedApp( @@ -73,30 +75,7 @@ class AppListActivity : ThemedActivity() { item = app binding.itemicon.setImageDrawable(app.icon) binding.title.text = app.name - if (forNeko) { - val packageName = app.packageName - val ver = getCachedApps()[packageName]?.versionName ?: "" - binding.desc.text = "$packageName ($ver)" - // - binding.button.isVisible = true - binding.button.setImageDrawable(getDrawable(R.drawable.ic_baseline_info_24)) - binding.button.setOnClickListener { - runOnIoDispatcher { - val jsi = NekoJSInterface(packageName) - jsi.init() - val about = jsi.getAbout() - jsi.destorySuspend() - Dialogs.message( - this@AppListActivity, app.name as String, - "PackageName: ${packageName}\n" + - "Version: ${ver}\n" + - "--------\n" + about - ) - } - } - } else { - binding.desc.text = "${app.packageName} (${app.uid})" - } + binding.desc.text = "${app.packageName} (${app.uid})" handlePayload(listOf(SWITCH)) } @@ -104,7 +83,6 @@ class AppListActivity : ThemedActivity() { if (payloads.contains(SWITCH)) { val selected = isProxiedApp(item) binding.itemcheck.isChecked = selected - binding.button.isVisible = forNeko && selected } } @@ -112,23 +90,6 @@ class AppListActivity : ThemedActivity() { if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true DataStore.routePackages = apps.filter { isProxiedApp(it) } .joinToString("\n") { it.packageName } - - if (forNeko) { - if (isProxiedApp(item)) { - runOnIoDispatcher { - try { - NekoPluginManager.installPlugin(item.packageName) - } catch (e: Exception) { - // failed UI - runOnUiThread { onClick(v) } - Dialogs.logExceptionAndShow(this@AppListActivity, e) { } - } - } - } else { - NekoPluginManager.removeManagedPlugin(item.packageName) - } - } - appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH) } } @@ -139,9 +100,10 @@ class AppListActivity : ThemedActivity() { var filteredApps = apps suspend fun reload() { - apps = getCachedApps().map { (packageName, packageInfo) -> + PackageCache.reload() + apps = cachedApps.mapNotNull { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() - ProxiedApp(packageManager, packageInfo.applicationInfo, packageName) + packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) } }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) } @@ -199,9 +161,12 @@ class AppListActivity : ThemedActivity() { private fun initProxiedUids(str: String = DataStore.routePackages) { proxiedUids.clear() - val apps = getCachedApps() - for (line in str.lineSequence()) proxiedUids[(apps[line] - ?: continue).applicationInfo.uid] = true + val apps = cachedApps + for (line in str.lineSequence()) { + val app = (apps[line] ?: continue) + val uid = app.applicationInfo?.uid ?: continue + proxiedUids[uid] = true + } } private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid] @@ -214,28 +179,29 @@ class AppListActivity : ThemedActivity() { val adapter = binding.list.adapter as AppsAdapter withContext(Dispatchers.IO) { adapter.reload() } adapter.filter.filter(binding.search.text?.toString() ?: "") - binding.list.crossFadeFrom(loading) - } - } - - private var forNeko = false - - fun getCachedApps(): MutableMap { - val packages = - if (forNeko) PackageCache.installedPluginPackages else PackageCache.installedPackages - return packages.toMutableMap().apply { - remove(BuildConfig.APPLICATION_ID) + if (apps.isEmpty()) { + binding.list.visibility = View.GONE + binding.appPlaceholder.root.crossFadeFrom(loading) + } else { + binding.list.crossFadeFrom(loading) + } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - forNeko = intent?.hasExtra(Key.NEKO_PLUGIN_MANAGED) == true binding = LayoutAppListBinding.inflate(layoutInflater) setContentView(binding.root) - ListHolderListener.setup(this) + binding.appPlaceholder.openSettings.setOnClickListener { + val intent = + Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = android.net.Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } + setSupportActionBar(binding.toolbar) supportActionBar?.apply { setTitle(R.string.select_apps) @@ -260,28 +226,13 @@ class AppListActivity : ThemedActivity() { appsAdapter.filter.filter(binding.search.text?.toString() ?: "") } - if (forNeko) { - DataStore.routePackages = DataStore.nekoPlugins - binding.search.setText(Plugins.AUTHORITIES_PREFIX_NEKO_PLUGIN) - } - - binding.searchLayout.isGone = forNeko - binding.hintNekoPlugin.isGone = !forNeko - binding.actionLearnMore.setOnClickListener { - launchCustomTab("https://matsuridayo.github.io/m-plugin/") - } - loadApps() } private var sysApps = false override fun onCreateOptionsMenu(menu: Menu): Boolean { - if (forNeko) { - menuInflater.inflate(R.menu.app_list_neko_menu, menu) - } else { - menuInflater.inflate(R.menu.app_list_menu, menu) - } + menuInflater.inflate(R.menu.app_list_menu, menu) return true } @@ -306,6 +257,7 @@ class AppListActivity : ThemedActivity() { return true } + R.id.action_clear_selections -> { runOnDefaultDispatcher { proxiedUids.clear() @@ -316,6 +268,7 @@ class AppListActivity : ThemedActivity() { } } } + R.id.action_export_clipboard -> { val success = SagerNet.trySetPrimaryClip("false\n${DataStore.routePackages}") Snackbar.make( @@ -325,6 +278,7 @@ class AppListActivity : ThemedActivity() { ).show() return true } + R.id.action_import_clipboard -> { val proxiedAppString = SagerNet.clipboard.primaryClip?.getItemAt(0)?.text?.toString() @@ -344,19 +298,6 @@ class AppListActivity : ThemedActivity() { } Snackbar.make(binding.list, R.string.action_import_err, Snackbar.LENGTH_LONG).show() } - R.id.uninstall_all -> { - runOnDefaultDispatcher { - proxiedUids.clear() - DataStore.routePackages = "" - apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) - NekoPluginManager.plugins.forEach { - NekoPluginManager.removeManagedPlugin(it) - } - onMainDispatcher { - appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH) - } - } - } } return super.onOptionsItemSelected(item) } @@ -375,7 +316,6 @@ class AppListActivity : ThemedActivity() { override fun onDestroy() { loader?.cancel() - if (forNeko) DataStore.nekoPlugins = DataStore.routePackages super.onDestroy() } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt index 75d2a6b33..effbc5823 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt @@ -3,15 +3,18 @@ package io.nekohasekai.sagernet.ui import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.Bundle +import android.text.TextUtils import android.util.SparseBooleanArray -import android.view.* +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.Filter import android.widget.Filterable -import android.widget.TextView import androidx.annotation.UiThread import androidx.core.util.contains import androidx.core.util.set @@ -30,24 +33,18 @@ import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.databinding.LayoutAppsBinding import io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding -import io.nekohasekai.sagernet.databinding.LayoutLoadingBinding import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ktx.crossFadeFrom import io.nekohasekai.sagernet.ktx.onMainDispatcher import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.utils.PackageCache -import io.nekohasekai.sagernet.widget.ListHolderListener import io.nekohasekai.sagernet.widget.ListListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext -import okhttp3.internal.closeQuietly -import org.jf.dexlib2.dexbacked.DexBackedDexFile -import org.jf.dexlib2.iface.DexFile -import java.io.File -import java.util.zip.ZipException -import java.util.zip.ZipFile +import moe.matsuri.nb4a.utils.NGUtil import kotlin.coroutines.coroutineContext class AppManagerActivity : ThemedActivity() { @@ -109,9 +106,10 @@ class AppManagerActivity : ThemedActivity() { var filteredApps = apps suspend fun reload() { - apps = cachedApps.map { (packageName, packageInfo) -> + PackageCache.reload() + apps = cachedApps.mapNotNull { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() - ProxiedApp(packageManager, packageInfo.applicationInfo, packageName) + packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) } }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) } @@ -145,7 +143,8 @@ class AppManagerActivity : ThemedActivity() { } override fun publishResults(constraint: CharSequence, results: FilterResults) { - @Suppress("UNCHECKED_CAST") filteredApps = results.values as List + @Suppress("UNCHECKED_CAST") + filteredApps = results.values as List notifyDataSetChanged() } } @@ -169,8 +168,11 @@ class AppManagerActivity : ThemedActivity() { private fun initProxiedUids(str: String = DataStore.individual) { proxiedUids.clear() val apps = cachedApps - for (line in str.lineSequence()) proxiedUids[(apps[line] - ?: continue).applicationInfo.uid] = true + for (line in str.lineSequence()) { + val app = (apps[line] ?: continue) + val uid = app.applicationInfo?.uid ?: continue + proxiedUids[uid] = true + } } private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid] @@ -183,7 +185,12 @@ class AppManagerActivity : ThemedActivity() { val adapter = binding.list.adapter as AppsAdapter withContext(Dispatchers.IO) { adapter.reload() } adapter.filter.filter(binding.search.text?.toString() ?: "") - binding.list.crossFadeFrom(loading) + if (apps.isEmpty()) { + binding.list.visibility = View.GONE + binding.appPlaceholder.root.crossFadeFrom(loading) + } else { + binding.list.crossFadeFrom(loading) + } } } @@ -193,7 +200,14 @@ class AppManagerActivity : ThemedActivity() { binding = LayoutAppsBinding.inflate(layoutInflater) setContentView(binding.root) - ListHolderListener.setup(this) + binding.appPlaceholder.openSettings.setOnClickListener { + val intent = + Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = android.net.Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } + setSupportActionBar(binding.toolbar) supportActionBar?.apply { setTitle(R.string.proxied_apps) @@ -212,10 +226,12 @@ class AppManagerActivity : ThemedActivity() { DataStore.proxyApps = false finish() } + R.id.appProxyModeOn -> DataStore.bypass = false R.id.appProxyModeBypass -> DataStore.bypass = true } } + binding.autoSelectProxyApps.setOnClickListener { selectProxyApp() } initProxiedUids() binding.list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) @@ -247,14 +263,11 @@ class AppManagerActivity : ThemedActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.action_scan_china_apps -> { - scanChinaApps() - return true - } R.id.action_invert_selections -> { runOnDefaultDispatcher { + val proxiedUidsOld = proxiedUids.clone() for (app in apps) { - if (proxiedUids.contains(app.uid)) { + if (proxiedUidsOld.contains(app.uid)) { proxiedUids.delete(app.uid) } else { proxiedUids[app.uid] = true @@ -270,6 +283,7 @@ class AppManagerActivity : ThemedActivity() { return true } + R.id.action_clear_selections -> { runOnDefaultDispatcher { proxiedUids.clear() @@ -280,8 +294,10 @@ class AppManagerActivity : ThemedActivity() { } } } + R.id.action_export_clipboard -> { - val success = SagerNet.trySetPrimaryClip("${DataStore.bypass}\n${DataStore.individual}") + val success = + SagerNet.trySetPrimaryClip("${DataStore.bypass}\n${DataStore.individual}") Snackbar.make( binding.list, if (success) R.string.action_export_msg else R.string.action_export_err, @@ -289,8 +305,10 @@ class AppManagerActivity : ThemedActivity() { ).show() return true } + R.id.action_import_clipboard -> { - val proxiedAppString = SagerNet.clipboard.primaryClip?.getItemAt(0)?.text?.toString() + val proxiedAppString = + SagerNet.clipboard.primaryClip?.getItemAt(0)?.text?.toString() if (!proxiedAppString.isNullOrEmpty()) { val i = proxiedAppString.indexOf('\n') try { @@ -316,150 +334,58 @@ class AppManagerActivity : ThemedActivity() { return super.onOptionsItemSelected(item) } - @SuppressLint("SetTextI18n") - private fun scanChinaApps() { - - val text: TextView - - val dialog = MaterialAlertDialogBuilder(this).setView( - LayoutLoadingBinding.inflate(layoutInflater).apply { - text = loadingText - }.root - ).setCancelable(false).show() - - val txt = text.text.toString() - - runOnDefaultDispatcher { - val chinaApps = ArrayList>() - val chinaRegex = ("(" + arrayOf( - "com.tencent", - "com.alibaba", - "com.umeng", - "com.qihoo", - "com.ali", - "com.alipay", - "com.amap", - "com.sina", - "com.weibo", - "com.vivo", - "com.xiaomi", - "com.huawei", - "com.taobao", - "com.secneo", - "s.h.e.l.l", - "com.stub", - "com.kiwisec", - "com.secshell", - "com.wrapper", - "cn.securitystack", - "com.mogosec", - "com.secoen", - "com.netease", - "com.mx", - "com.qq.e", - "com.baidu", - "com.bytedance", - "com.bugly", - "com.miui", - "com.oppo", - "com.coloros", - "com.iqoo", - "com.meizu", - "com.gionee", - "cn.nubia" - ).joinToString("|") { "${it.replace(".", "\\.")}\\." } + ").*").toRegex() - - val bypass = DataStore.bypass - val cachedApps = cachedApps - - apps = cachedApps.map { (packageName, packageInfo) -> - kotlin.coroutines.coroutineContext[Job]!!.ensureActive() - ProxiedApp(packageManager, packageInfo.applicationInfo, packageName) - }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) - - scan@ for ((pkg, app) in cachedApps.entries) { - /*if (!sysApps && app.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) { - continue - }*/ - - val index = appsAdapter.filteredApps.indexOfFirst { it.uid == app.applicationInfo.uid } - var changed = false - - onMainDispatcher { - text.text = (txt + " " + app.packageName + "\n\n" + chinaApps.map { it.second } - .reversed() - .joinToString("\n", postfix = "\n")).trim() - } - + private fun selectProxyApp() { + MaterialAlertDialogBuilder(this).setTitle(R.string.confirm) + .setMessage(R.string.auto_select_proxy_apps_message) + .setPositiveButton(R.string.yes) { _, _ -> try { - - val dex = File(app.applicationInfo.publicSourceDir) - val zipFile = ZipFile(dex) - var dexFile: DexFile - - for (entry in zipFile.entries()) { - if (entry.name.startsWith("classes") && entry.name.endsWith(".dex")) { - val input = zipFile.getInputStream(entry).readBytes() - dexFile = try { - DexBackedDexFile.fromInputStream(null, input.inputStream()) - } catch (e: Exception) { - Logs.w(e) - break + val needProxyAppsList = getAutoProxyApps("") + val bypass = DataStore.bypass + proxiedUids.clear() + for (app in cachedApps) { + val needProxy = + needProxyAppsList.contains(app.key) || (app.value.applicationInfo?.uid + ?: 0) == 1000 + if (needProxy) { + if (!bypass) { + app.value.applicationInfo?.apply { + proxiedUids[uid] = true + } } - for (clazz in dexFile.classes) { - val clazzName = clazz.type.substring(1, clazz.type.length - 1) - .replace("/", ".") - .replace("$", ".") - - if (clazzName.matches(chinaRegex)) { - chinaApps.add( - app to app.applicationInfo.loadLabel(packageManager) - .toString() - ) - zipFile.closeQuietly() - - if (bypass) { - changed = !proxiedUids[app.applicationInfo.uid] - proxiedUids[app.applicationInfo.uid] = true - } else { - proxiedUids.delete(app.applicationInfo.uid) - } - - continue@scan + } else { + if (bypass) { + app.value.applicationInfo?.apply { + proxiedUids[uid] = true } } } } - zipFile.closeQuietly() - - if (bypass) { - proxiedUids.delete(app.applicationInfo.uid) - } else { - changed = !proxiedUids[index] - proxiedUids[app.applicationInfo.uid] = true - } - - } catch (e: ZipException) { - Logs.w("Error in pkg ${app.packageName}:${app.versionName}", e) - continue + DataStore.individual = + apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName } + apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) + appsAdapter.filter.filter(binding.search.text?.toString() ?: "") + } catch (e: Exception) { + Logs.e(e) } - } + .setNegativeButton(R.string.no, null) + .show() + } - DataStore.individual = apps.filter { isProxiedApp(it) } - .joinToString("\n") { it.packageName } - - apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) - - onMainDispatcher { - appsAdapter.filter.filter(binding.search.text?.toString() ?: "") - - dialog.dismiss() + private fun getAutoProxyApps(content: String): List { + var list = listOf() + try { + val proxyApps = if (TextUtils.isEmpty(content)) { + NGUtil.readTextFromAssets(app, "proxy_packagename.txt") + } else { + content } - + if (!TextUtils.isEmpty(proxyApps)) { + list = proxyApps.split("\n") + } + } catch (_: Exception) { } - - + return list } override fun supportNavigateUpTo(upIntent: Intent) = diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt index 21f22c696..d63848289 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt @@ -18,6 +18,7 @@ import io.nekohasekai.sagernet.databinding.LayoutAssetsBinding import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.widget.UndoSnackbarManager import libcore.Libcore +import moe.matsuri.nb4a.utils.Util import org.json.JSONObject import java.io.File import java.io.FileWriter @@ -231,7 +232,7 @@ class AssetsActivity : ThemedActivity() { updateAsset(file, versionFile, localVersion) }.onFailure { onMainDispatcher { - alert(it.readableMessage).show() + alert(it.readableMessage).tryToShow() } } @@ -249,23 +250,28 @@ class AssetsActivity : ThemedActivity() { } + private val rulesProviders = listOf( + RuleAssetsProvider( + "SagerNet/sing-geoip", + "SagerNet/sing-geosite", + ), + RuleAssetsProvider( + "soffchen/sing-geoip", + "soffchen/sing-geosite", + ), + RuleAssetsProvider( + "Chocolate4U/Iran-sing-box-rules" + ), + RuleAssetsProvider( + "L11R/antizapret-sing-box-geo" + ), + ) + suspend fun updateAsset(file: File, versionFile: File, localVersion: String) { - val repo: String var fileName = file.name - if (DataStore.rulesProvider == 0) { - if (file.name == assetNames[0]) { - repo = "SagerNet/sing-geoip" - } else { - repo = "SagerNet/sing-geosite" - } - } else { - if (file.name == assetNames[0]) { - repo = "soffchen/sing-geoip" - } else { - repo = "soffchen/sing-geosite" - } - } + val ruleProvider = rulesProviders[DataStore.rulesProvider] + val repo = ruleProvider.repoByFileName[fileName] val client = Libcore.newHttpClient().apply { modernTLS() @@ -278,7 +284,7 @@ class AssetsActivity : ThemedActivity() { setURL("https://api.github.com/repos/$repo/releases/latest") }.execute() - val release = JSONObject(response.contentString) + val release = JSONObject(Util.getStringBox(response.contentString)) val tagName = release.optString("tag_name") if (tagName == localVersion) { @@ -338,5 +344,17 @@ class AssetsActivity : ThemedActivity() { } } - + private data class RuleAssetsProvider( + val repoByFileName: Map + ) { + constructor( + geoipRepo: String, + geositeRepo: String = geoipRepo, + ) : this( + mapOf( + "geoip.db" to geoipRepo, + "geosite.db" to geositeRepo, + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt index 46ac0ec4b..b439225f8 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt @@ -16,6 +16,7 @@ import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.bg.Executable import io.nekohasekai.sagernet.database.* import io.nekohasekai.sagernet.database.preference.KeyValuePair import io.nekohasekai.sagernet.database.preference.PublicDatabase @@ -23,6 +24,7 @@ import io.nekohasekai.sagernet.databinding.LayoutBackupBinding import io.nekohasekai.sagernet.databinding.LayoutImportBinding import io.nekohasekai.sagernet.databinding.LayoutProgressBinding import io.nekohasekai.sagernet.ktx.* +import kotlinx.coroutines.delay import moe.matsuri.nb4a.utils.Util import org.json.JSONArray import org.json.JSONObject @@ -34,33 +36,45 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) { override fun name0() = app.getString(R.string.backup) var content = "" - private val exportSettings = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data -> - if (data != null) { - runOnDefaultDispatcher { - try { - requireActivity().contentResolver.openOutputStream( - data - )!!.bufferedWriter().use { - it.write(content) - } - onMainDispatcher { - snackbar(getString(R.string.action_export_msg)).show() - } - } catch (e: Exception) { - Logs.w(e) - onMainDispatcher { - snackbar(e.readableMessage).show() + private val exportSettings = + registerForActivityResult(ActivityResultContracts.CreateDocument()) { data -> + if (data != null) { + runOnDefaultDispatcher { + try { + requireActivity().contentResolver.openOutputStream( + data + )!!.bufferedWriter().use { + it.write(content) + } + onMainDispatcher { + snackbar(getString(R.string.action_export_msg)).show() + } + } catch (e: Exception) { + Logs.w(e) + onMainDispatcher { + snackbar(e.readableMessage).show() + } } } - } } - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = LayoutBackupBinding.bind(view) + + binding.resetSettings.setOnClickListener { + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm) + .setMessage(R.string.reset_settings_message) + .setNegativeButton(R.string.no, null) + .setPositiveButton(R.string.yes) { _, _ -> + DataStore.configurationStore.reset() + triggerFullRestart(requireContext()) + } + .show() + } + binding.actionExport.setOnClickListener { runOnDefaultDispatcher { content = doBackup( @@ -230,13 +244,11 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) { import.backupRules.isChecked, import.backupSettings.isChecked ) - ProcessPhoenix.triggerRebirth( - requireContext(), Intent(requireContext(), MainActivity::class.java) - ) + triggerFullRestart(requireContext()) }.onFailure { Logs.w(it) onMainDispatcher { - alert(it.readableMessage).show() + alert(it.readableMessage).tryToShow() } } 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 50ae989bd..054091075 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -1,8 +1,8 @@ package io.nekohasekai.sagernet.ui +import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color -import android.net.Uri import android.os.Bundle import android.os.SystemClock import android.provider.OpenableColumns @@ -10,15 +10,19 @@ import android.text.SpannableStringBuilder import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.format.Formatter import android.text.style.ForegroundColorSpan -import android.view.* +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.core.net.toUri import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.size @@ -32,45 +36,84 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import io.nekohasekai.sagernet.* +import io.nekohasekai.sagernet.GroupOrder +import io.nekohasekai.sagernet.GroupType +import io.nekohasekai.sagernet.Key +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.aidl.TrafficData import io.nekohasekai.sagernet.bg.BaseService import io.nekohasekai.sagernet.bg.proto.UrlTest -import io.nekohasekai.sagernet.database.* +import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.database.GroupManager +import io.nekohasekai.sagernet.database.ProfileManager +import io.nekohasekai.sagernet.database.ProxyEntity +import io.nekohasekai.sagernet.database.ProxyGroup +import io.nekohasekai.sagernet.database.SagerDatabase import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener -import io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding import io.nekohasekai.sagernet.databinding.LayoutProfileListBinding import io.nekohasekai.sagernet.databinding.LayoutProgressListBinding import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.fmt.toUniversalLink +import io.nekohasekai.sagernet.group.GroupUpdater import io.nekohasekai.sagernet.group.RawUpdater -import io.nekohasekai.sagernet.ktx.* +import io.nekohasekai.sagernet.ktx.FixedLinearLayoutManager +import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.SubscriptionFoundException +import io.nekohasekai.sagernet.ktx.alert +import io.nekohasekai.sagernet.ktx.app +import io.nekohasekai.sagernet.ktx.dp2px +import io.nekohasekai.sagernet.ktx.getColorAttr +import io.nekohasekai.sagernet.ktx.getColour +import io.nekohasekai.sagernet.ktx.isIpAddress +import io.nekohasekai.sagernet.ktx.onMainDispatcher +import io.nekohasekai.sagernet.ktx.readableMessage +import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher +import io.nekohasekai.sagernet.ktx.runOnLifecycleDispatcher +import io.nekohasekai.sagernet.ktx.runOnMainDispatcher +import io.nekohasekai.sagernet.ktx.scrollTo +import io.nekohasekai.sagernet.ktx.showAllowingStateLoss +import io.nekohasekai.sagernet.ktx.snackbar +import io.nekohasekai.sagernet.ktx.startFilesForResult +import io.nekohasekai.sagernet.ktx.tryToShow import io.nekohasekai.sagernet.plugin.PluginManager -import io.nekohasekai.sagernet.ui.profile.* -import io.nekohasekai.sagernet.utils.PackageCache +import io.nekohasekai.sagernet.ui.profile.ChainSettingsActivity +import io.nekohasekai.sagernet.ui.profile.HttpSettingsActivity +import io.nekohasekai.sagernet.ui.profile.HysteriaSettingsActivity +import io.nekohasekai.sagernet.ui.profile.MieruSettingsActivity +import io.nekohasekai.sagernet.ui.profile.NaiveSettingsActivity +import io.nekohasekai.sagernet.ui.profile.SSHSettingsActivity +import io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity +import io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity +import io.nekohasekai.sagernet.ui.profile.TrojanGoSettingsActivity +import io.nekohasekai.sagernet.ui.profile.TrojanSettingsActivity +import io.nekohasekai.sagernet.ui.profile.TuicSettingsActivity +import io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity +import io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity import io.nekohasekai.sagernet.widget.QRCodeDialog import io.nekohasekai.sagernet.widget.UndoSnackbarManager -import kotlinx.coroutines.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.matsuri.nb4a.Protocols import moe.matsuri.nb4a.Protocols.getProtocolColor -import moe.matsuri.nb4a.plugin.NekoPluginManager +import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity -import moe.matsuri.nb4a.proxy.neko.NekoJSInterface -import moe.matsuri.nb4a.proxy.neko.NekoSettingActivity -import moe.matsuri.nb4a.proxy.neko.canShare import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity +import moe.matsuri.nb4a.ui.ConnectionTestNotification import okhttp3.internal.closeQuietly -import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket import java.net.UnknownHostException -import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicInteger import java.util.zip.ZipInputStream -import kotlin.collections.set class ConfigurationFragment @JvmOverloads constructor( val select: Boolean = false, val selectedItem: ProxyEntity? = null, val titleRes: Int = 0 @@ -91,7 +134,12 @@ class ConfigurationFragment @JvmOverloads constructor( val alwaysShowAddress by lazy { DataStore.alwaysShowAddress } fun getCurrentGroupFragment(): GroupFragment? { - return childFragmentManager.findFragmentByTag("f" + DataStore.selectedGroup) as GroupFragment? + return try { + childFragmentManager.findFragmentByTag("f" + DataStore.selectedGroup) as GroupFragment? + } catch (e: Exception) { + Logs.e(e) + null + } } val updateSelectedCallback = object : ViewPager2.OnPageChangeCallback() { @@ -111,6 +159,7 @@ class ConfigurationFragment @JvmOverloads constructor( override fun onQueryTextSubmit(query: String): Boolean = false + @SuppressLint("DetachAndAttachSameFragment") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -141,6 +190,12 @@ class ConfigurationFragment @JvmOverloads constructor( if (searchView != null) { searchView.setOnQueryTextListener(this) searchView.maxWidth = Int.MAX_VALUE + + searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + cancelSearch(searchView) + } + } } groupPager = view.findViewById(R.id.group_pager) @@ -262,7 +317,7 @@ class ConfigurationFragment @JvmOverloads constructor( snackbar(getString(R.string.no_proxies_found_in_file)).show() } else import(proxies) } catch (e: SubscriptionFoundException) { - (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link)) + (requireActivity() as MainActivity).importSubscription(e.link.toUri()) } catch (e: Exception) { Logs.w(e) onMainDispatcher { @@ -293,6 +348,7 @@ class ConfigurationFragment @JvmOverloads constructor( R.id.action_scan_qr_code -> { startActivity(Intent(context, ScannerActivity::class.java)) } + R.id.action_import_clipboard -> { val text = SagerNet.getClipboardText() if (text.isBlank()) { @@ -304,7 +360,7 @@ class ConfigurationFragment @JvmOverloads constructor( snackbar(getString(R.string.no_proxies_found_in_clipboard)).show() } else import(proxies) } catch (e: SubscriptionFoundException) { - (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link)) + (requireActivity() as MainActivity).importSubscription(e.link.toUri()) } catch (e: Exception) { Logs.w(e) @@ -314,87 +370,93 @@ class ConfigurationFragment @JvmOverloads constructor( } } } + R.id.action_import_file -> { startFilesForResult(importFile, "*/*") } + R.id.action_new_socks -> { startActivity(Intent(requireActivity(), SocksSettingsActivity::class.java)) } + R.id.action_new_http -> { startActivity(Intent(requireActivity(), HttpSettingsActivity::class.java)) } + R.id.action_new_ss -> { startActivity(Intent(requireActivity(), ShadowsocksSettingsActivity::class.java)) } + R.id.action_new_vmess -> { startActivity(Intent(requireActivity(), VMessSettingsActivity::class.java)) } + R.id.action_new_vless -> { startActivity(Intent(requireActivity(), VMessSettingsActivity::class.java).apply { putExtra("vless", true) }) } + R.id.action_new_trojan -> { startActivity(Intent(requireActivity(), TrojanSettingsActivity::class.java)) } + R.id.action_new_trojan_go -> { startActivity(Intent(requireActivity(), TrojanGoSettingsActivity::class.java)) } + + R.id.action_new_mieru -> { + startActivity(Intent(requireActivity(), MieruSettingsActivity::class.java)) + } + R.id.action_new_naive -> { startActivity(Intent(requireActivity(), NaiveSettingsActivity::class.java)) } + R.id.action_new_hysteria -> { startActivity(Intent(requireActivity(), HysteriaSettingsActivity::class.java)) } + R.id.action_new_tuic -> { startActivity(Intent(requireActivity(), TuicSettingsActivity::class.java)) } + R.id.action_new_ssh -> { startActivity(Intent(requireActivity(), SSHSettingsActivity::class.java)) } + R.id.action_new_wg -> { startActivity(Intent(requireActivity(), WireGuardSettingsActivity::class.java)) } + R.id.action_new_shadowtls -> { startActivity(Intent(requireActivity(), ShadowTLSSettingsActivity::class.java)) } + + R.id.action_new_anytls -> { + startActivity(Intent(requireActivity(), AnyTLSSettingsActivity::class.java)) + } + R.id.action_new_config -> { startActivity(Intent(requireActivity(), ConfigSettingActivity::class.java)) } + R.id.action_new_chain -> { startActivity(Intent(requireActivity(), ChainSettingsActivity::class.java)) } - R.id.action_new_neko -> { - val context = requireContext() - lateinit var dialog: AlertDialog - val linearLayout = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - - NekoPluginManager.getProtocols().forEach { obj -> - LayoutAppsItemBinding.inflate(layoutInflater, this, true).apply { - itemcheck.isGone = true - button.isGone = false - itemicon.setImageDrawable( - PackageCache.installedApps[obj.plgId]?.loadIcon( - context.packageManager - ) - ) - title.text = obj.protocolId - desc.text = obj.plgId - button.setOnClickListener { - dialog.dismiss() - val intent = Intent(context, NekoSettingActivity::class.java) - intent.putExtra("plgId", obj.plgId) - intent.putExtra("protocolId", obj.protocolId) - startActivity(intent) - } - } + + R.id.action_update_subscription -> { + val group = DataStore.currentGroup() + if (group.type != GroupType.SUBSCRIPTION) { + snackbar(R.string.group_not_subscription).show() + Logs.e("onMenuItemClick: Group(${group.displayName()}) is not subscription") + } else { + runOnLifecycleDispatcher { + GroupUpdater.startUpdate(group, true) } } - dialog = MaterialAlertDialogBuilder(context).setTitle(R.string.neko_plugin) - .setView(linearLayout) - .show() } + R.id.action_clear_traffic_statistics -> { runOnDefaultDispatcher { val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId()) @@ -411,6 +473,7 @@ class ConfigurationFragment @JvmOverloads constructor( } } } + R.id.action_connection_test_clear_results -> { runOnDefaultDispatcher { val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId()) @@ -428,6 +491,7 @@ class ConfigurationFragment @JvmOverloads constructor( } } } + R.id.action_connection_test_delete_unavailable -> { runOnDefaultDispatcher { val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId()) @@ -466,6 +530,7 @@ class ConfigurationFragment @JvmOverloads constructor( } } } + R.id.action_remove_duplicate -> { runOnDefaultDispatcher { val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId()) @@ -517,9 +582,11 @@ class ConfigurationFragment @JvmOverloads constructor( } } } + R.id.action_connection_tcp_ping -> { pingTest(false) } + R.id.action_connection_url_test -> { urlTest() } @@ -530,28 +597,41 @@ class ConfigurationFragment @JvmOverloads constructor( inner class TestDialog { val binding = LayoutProgressListBinding.inflate(layoutInflater) val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root) - .setNegativeButton(android.R.string.cancel) { _, _ -> - cancel() + .setPositiveButton(R.string.minimize) { _, _ -> + minimize() } - .setOnDismissListener { + .setNegativeButton(android.R.string.cancel) { _, _ -> cancel() } .setCancelable(false) lateinit var cancel: () -> Unit - val fragment by lazy { getCurrentGroupFragment() } - val results = Collections.synchronizedList(mutableListOf()) + lateinit var minimize: () -> Unit + + val dialogStatus = AtomicInteger(0) // 1: hidden 2: cancelled + var notification: ConnectionTestNotification? = null + + val results: MutableSet = ConcurrentHashMap.newKeySet() var proxyN = 0 val finishedN = AtomicInteger(0) - suspend fun insert(profile: ProxyEntity?) { - results.add(profile) - } + fun update(profile: ProxyEntity) { + if (dialogStatus.get() != 2) { + results.add(profile) + } + runOnMainDispatcher { + val context = context ?: return@runOnMainDispatcher + val progress = finishedN.addAndGet(1) + val status = dialogStatus.get() + notification?.updateNotification( + progress, + proxyN, + progress >= proxyN || status == 2 + ) + if (status >= 1) return@runOnMainDispatcher + if (!isAdded) return@runOnMainDispatcher - suspend fun update(profile: ProxyEntity) { - fragment?.configurationListView?.post { - val context = context ?: return@post - if (!isAdded) return@post + // refresh dialog var profileStatusText: String? = null var profileStatusColor = 0 @@ -561,18 +641,22 @@ class ConfigurationFragment @JvmOverloads constructor( profileStatusText = profile.error profileStatusColor = context.getColorAttr(android.R.attr.textColorSecondary) } + 0 -> { profileStatusText = getString(R.string.connection_test_testing) profileStatusColor = context.getColorAttr(android.R.attr.textColorSecondary) } + 1 -> { profileStatusText = getString(R.string.available, profile.ping) profileStatusColor = context.getColour(R.color.material_green_500) } + 2 -> { profileStatusText = profile.error profileStatusColor = context.getColour(R.color.material_red_500) } + 3 -> { val err = profile.error ?: "" val msg = Protocols.genFriendlyMsg(err) @@ -599,64 +683,46 @@ class ConfigurationFragment @JvmOverloads constructor( } binding.nowTesting.text = text - binding.progress.text = "${finishedN.addAndGet(1)} / $proxyN" + binding.progress.text = "$progress / $proxyN" } } } - fun stopService() { - if (DataStore.serviceState.started) SagerNet.stopService() - } - @OptIn(DelicateCoroutinesApi::class) @Suppress("EXPERIMENTAL_API_USAGE") fun pingTest(icmpPing: Boolean) { + if (DataStore.runningTest) return else DataStore.runningTest = true val test = TestDialog() - val testJobs = mutableListOf() val dialog = test.builder.show() + val testJobs = mutableListOf() + val group = DataStore.currentGroup() + val mainJob = runOnDefaultDispatcher { - if (DataStore.serviceState.started) { - stopService() - delay(500) // wait for service stop - } - val group = DataStore.currentGroup() - val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id) - test.proxyN = profilesUnfiltered.size - val profiles = ConcurrentLinkedQueue(profilesUnfiltered) - val testPool = newFixedThreadPoolContext( - DataStore.connectionTestConcurrent, - "pingTest" - ) + val profilesList = SagerDatabase.proxyDao.getByGroup(group.id).filter { + if (icmpPing) { + if (it.requireBean().canICMPing()) { + return@filter true + } + } else { + if (it.requireBean().canTCPing()) { + return@filter true + } + } + return@filter false + } + test.proxyN = profilesList.size + val profiles = ConcurrentLinkedQueue(profilesList) repeat(DataStore.connectionTestConcurrent) { - testJobs.add(launch(testPool) { + testJobs.add(launch(Dispatchers.IO) { while (isActive) { val profile = profiles.poll() ?: break - if (icmpPing) { - if (!profile.requireBean().canICMPing()) { - profile.status = -1 - profile.error = - app.getString(R.string.connection_test_icmp_ping_unavailable) - test.insert(profile) - continue - } - } else { - if (!profile.requireBean().canTCPing()) { - profile.status = -1 - profile.error = - app.getString(R.string.connection_test_tcp_ping_unavailable) - test.insert(profile) - continue - } - } - profile.status = 0 - test.insert(profile) var address = profile.requireBean().serverAddress if (!address.isIpAddress()) { try { - InetAddress.getAllByName(address).apply { + SagerNet.underlyingNetwork!!.getAllByName(address).apply { if (isNotEmpty()) { address = this[0].hostAddress } @@ -675,7 +741,9 @@ class ConfigurationFragment @JvmOverloads constructor( if (icmpPing) { // removed } else { - val socket = Socket() + val socket = + SagerNet.underlyingNetwork?.socketFactory?.createSocket() + ?: Socket() try { socket.soTimeout = 3000 socket.bind(InetSocketAddress(0)) @@ -705,15 +773,18 @@ class ConfigurationFragment @JvmOverloads constructor( when { !message.contains("failed:") -> profile.error = getString(R.string.connection_test_timeout) + else -> when { message.contains("ECONNREFUSED") -> { profile.error = getString(R.string.connection_test_refused) } + message.contains("ENETUNREACH") -> { profile.error = getString(R.string.connection_test_unreachable) } + else -> { profile.status = 3 profile.error = message @@ -728,15 +799,18 @@ class ConfigurationFragment @JvmOverloads constructor( } testJobs.joinAll() - testPool.close() - onMainDispatcher { - dialog.dismiss() + runOnMainDispatcher { + test.cancel() } } test.cancel = { + test.dialogStatus.set(2) + dialog.dismiss() runOnDefaultDispatcher { - test.results.filterNotNull().forEach { + mainJob.cancel() + testJobs.forEach { it.cancel() } + test.results.forEach { try { ProfileManager.updateProfile(it) } catch (e: Exception) { @@ -744,38 +818,37 @@ class ConfigurationFragment @JvmOverloads constructor( } } GroupManager.postReload(DataStore.currentGroupId()) - mainJob.cancel() - testJobs.forEach { it.cancel() } + DataStore.runningTest = false } } + test.minimize = { + test.dialogStatus.set(1) + test.notification = ConnectionTestNotification( + dialog.context, + "[${group.displayName()}] ${getString(R.string.connection_test)}" + ) + dialog.hide() + } } @OptIn(DelicateCoroutinesApi::class) fun urlTest() { + if (DataStore.runningTest) return else DataStore.runningTest = true val test = TestDialog() val dialog = test.builder.show() val testJobs = mutableListOf() + val group = DataStore.currentGroup() val mainJob = runOnDefaultDispatcher { - if (DataStore.serviceState.started) { - stopService() - delay(500) // wait for service stop - } - val group = DataStore.currentGroup() - val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id) - test.proxyN = profilesUnfiltered.size - val profiles = ConcurrentLinkedQueue(profilesUnfiltered) - val testPool = newFixedThreadPoolContext( - DataStore.connectionTestConcurrent, - "urlTest" - ) + val profilesList = SagerDatabase.proxyDao.getByGroup(group.id) + test.proxyN = profilesList.size + val profiles = ConcurrentLinkedQueue(profilesList) repeat(DataStore.connectionTestConcurrent) { - testJobs.add(launch(testPool) { + testJobs.add(launch(Dispatchers.IO) { val urlTest = UrlTest() // note: this is NOT in bg process while (isActive) { val profile = profiles.poll() ?: break profile.status = 0 - test.insert(profile) try { val result = urlTest.doTest(profile) @@ -796,13 +869,17 @@ class ConfigurationFragment @JvmOverloads constructor( testJobs.joinAll() - onMainDispatcher { - dialog.dismiss() + runOnMainDispatcher { + test.cancel() } } test.cancel = { + test.dialogStatus.set(2) + dialog.dismiss() runOnDefaultDispatcher { - test.results.filterNotNull().forEach { + mainJob.cancel() + testJobs.forEach { it.cancel() } + test.results.forEach { try { ProfileManager.updateProfile(it) } catch (e: Exception) { @@ -810,11 +887,17 @@ class ConfigurationFragment @JvmOverloads constructor( } } GroupManager.postReload(DataStore.currentGroupId()) - NekoJSInterface.Default.destroyAllJsi() - mainJob.cancel() - testJobs.forEach { it.cancel() } + DataStore.runningTest = false } } + test.minimize = { + test.dialogStatus.set(1) + test.notification = ConnectionTestNotification( + dialog.context, + "[${group.displayName()}] ${getString(R.string.connection_test)}" + ) + dialog.hide() + } } inner class GroupPagerAdapter : FragmentStateAdapter(this), @@ -855,16 +938,18 @@ class ConfigurationFragment @JvmOverloads constructor( } } - val runFunc = if (now) requireActivity()::runOnUiThread else groupPager::post - runFunc { - groupList = newGroupList - notifyDataSetChanged() - if (set) groupPager.setCurrentItem(selectedGroupIndex, false) - val hideTab = groupList.size < 2 - tabLayout.isGone = hideTab - toolbar.elevation = if (hideTab) 0F else dp2px(4).toFloat() - if (!select) { - groupPager.registerOnPageChangeCallback(updateSelectedCallback) + val runFunc = if (now) activity?.let { it::runOnUiThread } else groupPager::post + if (runFunc != null) { + runFunc { + groupList = newGroupList + notifyDataSetChanged() + if (set) groupPager.setCurrentItem(selectedGroupIndex, false) + val hideTab = groupList.size < 2 + tabLayout.isGone = hideTab + toolbar.elevation = if (hideTab) 0F else dp2px(4).toFloat() + if (!select) { + groupPager.registerOnPageChangeCallback(updateSelectedCallback) + } } } } @@ -939,7 +1024,7 @@ class ConfigurationFragment @JvmOverloads constructor( override suspend fun onUpdated(data: TrafficData) = Unit - override suspend fun onUpdated(profile: ProxyEntity) = Unit + override suspend fun onUpdated(profile: ProxyEntity, noTraffic: Boolean) = Unit override suspend fun onRemoved(groupId: Long, profileId: Long) { val group = groupList.find { it.id == groupId } ?: return @@ -1034,9 +1119,11 @@ class ConfigurationFragment @JvmOverloads constructor( GroupOrder.ORIGIN -> { origin.isChecked = true } + GroupOrder.BY_NAME -> { byName.isChecked = true } + GroupOrder.BY_DELAY -> { byDelay.isChecked = true } @@ -1267,7 +1354,7 @@ class ConfigurationFragment @JvmOverloads constructor( } } - override suspend fun onUpdated(profile: ProxyEntity) { + override suspend fun onUpdated(profile: ProxyEntity, noTraffic: Boolean) { if (profile.groupId != proxyGroup.id) return val index = configurationIdList.indexOf(profile.id) if (index < 0) return @@ -1275,9 +1362,21 @@ class ConfigurationFragment @JvmOverloads constructor( if (::undoManager.isInitialized) { undoManager.flush() } - val oldProfile = configurationList[profile.id] configurationList[profile.id] = profile notifyItemChanged(index) + // + val oldProfile = configurationList[profile.id] + if (noTraffic && oldProfile != null) { + runOnDefaultDispatcher { + onUpdated( + TrafficData( + id = profile.id, + rx = oldProfile.rx, + tx = oldProfile.tx + ) + ) + } + } } } @@ -1327,12 +1426,12 @@ class ConfigurationFragment @JvmOverloads constructor( fun reloadProfiles() { var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id) - val subscription = proxyGroup.subscription when (proxyGroup.order) { GroupOrder.BY_NAME -> { newProfiles = newProfiles.sortedBy { it.displayName() } } + GroupOrder.BY_DELAY -> { newProfiles = newProfiles.sortedBy { if (it.status == 1) it.ping else 114514 } @@ -1486,7 +1585,7 @@ class ConfigurationFragment @JvmOverloads constructor( val msg = Protocols.genFriendlyMsg(err) profileStatus.text = if (msg != err) msg else getString(R.string.unavailable) profileStatus.setOnClickListener { - alert(err).show() + alert(err).tryToShow() } } else { profileStatus.setOnClickListener(null) @@ -1514,7 +1613,7 @@ class ConfigurationFragment @JvmOverloads constructor( removeButton.isGone = select proxyEntity.nekoBean?.apply { - shareLayout.isGone = !canShare() + shareLayout.isGone = true } runOnDefaultDispatcher { @@ -1538,6 +1637,7 @@ class ConfigurationFragment @JvmOverloads constructor( R.id.action_standard_clipboard ) } + !proxyEntity.haveLink() -> { popup.menu.removeItem(R.id.action_group_qr) popup.menu.removeItem(R.id.action_group_clipboard) @@ -1583,12 +1683,13 @@ class ConfigurationFragment @JvmOverloads constructor( try { currentName = entity.displayName()!! when (item.itemId) { - R.id.action_standard_qr -> showCode(entity.toStdLink()!!) - R.id.action_standard_clipboard -> export(entity.toStdLink()!!) + R.id.action_standard_qr -> showCode(entity.toStdLink()) + R.id.action_standard_clipboard -> export(entity.toStdLink()) R.id.action_universal_qr -> showCode(entity.requireBean().toUniversalLink()) R.id.action_universal_clipboard -> export( entity.requireBean().toUniversalLink() ) + R.id.action_config_export_clipboard -> export(entity.exportConfig().first) R.id.action_config_export_file -> { val cfg = entity.exportConfig() @@ -1633,4 +1734,9 @@ class ConfigurationFragment @JvmOverloads constructor( } } -} \ No newline at end of file + private fun cancelSearch(searchView: SearchView) { + searchView.onActionViewCollapsed() + searchView.clearFocus() + } + +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt deleted file mode 100644 index a5d33a35f..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.nekohasekai.sagernet.ui - -import android.os.Bundle -import android.view.View -import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.databinding.LayoutDebugBinding -import io.nekohasekai.sagernet.ktx.snackbar - -class DebugFragment : NamedFragment(R.layout.layout_debug) { - - override fun name0() = "Debug" - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val binding = LayoutDebugBinding.bind(view) - - binding.debugCrash.setOnClickListener { - error("test crash") - } - binding.resetSettings.setOnClickListener { - DataStore.configurationStore.reset() - snackbar("Cleared").show() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt index 6c063d342..ffc1f55ba 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt @@ -23,12 +23,13 @@ import io.nekohasekai.sagernet.databinding.LayoutGroupItemBinding import io.nekohasekai.sagernet.fmt.toUniversalLink import io.nekohasekai.sagernet.group.GroupUpdater import io.nekohasekai.sagernet.ktx.* -import io.nekohasekai.sagernet.widget.ListHolderListener +import io.nekohasekai.sagernet.widget.ListListener import io.nekohasekai.sagernet.widget.QRCodeDialog import io.nekohasekai.sagernet.widget.UndoSnackbarManager import kotlinx.coroutines.delay import moe.matsuri.nb4a.utils.Util import moe.matsuri.nb4a.utils.toBytesString +import java.lang.NumberFormatException import java.util.* class GroupFragment : ToolbarFragment(R.layout.layout_group), @@ -44,7 +45,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), super.onViewCreated(view, savedInstanceState) activity = requireActivity() as MainActivity - ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener) + ViewCompat.setOnApplyWindowInsetsListener(view, ListListener) toolbar.setTitle(R.string.menu_group) toolbar.inflateMenu(R.menu.add_group_menu) toolbar.setOnMenuItemClickListener(this) @@ -111,6 +112,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), R.id.action_new_group -> { startActivity(Intent(context, GroupSettingsActivity::class.java)) } + R.id.action_update_all -> { MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm) .setMessage(R.string.update_all_subscription) @@ -339,9 +341,11 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), proxyGroup.toUniversalLink(), proxyGroup.displayName() ).showAllowingStateLoss(parentFragmentManager) } + R.id.action_universal_clipboard -> { export(proxyGroup.toUniversalLink()) } + R.id.action_export_clipboard -> { runOnDefaultDispatcher { val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id) @@ -352,9 +356,11 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), } } } + R.id.action_export_file -> { startFilesForResult(exportProfiles, "profiles_${proxyGroup.displayName()}.txt") } + R.id.action_clear -> { MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm) .setMessage(R.string.clear_profiles_message) @@ -415,9 +421,10 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), subscriptionUpdateProgress.isIndeterminate = true } else { subscriptionUpdateProgress.isIndeterminate = false - val progress = GroupUpdater.progress[proxyGroup.id]!! - subscriptionUpdateProgress.max = progress.max - subscriptionUpdateProgress.progress = progress.progress + GroupUpdater.progress[proxyGroup.id]?.let { + subscriptionUpdateProgress.max = it.max + subscriptionUpdateProgress.progress = it.progress + } } updateButton.isInvisible = true @@ -436,17 +443,17 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), if (subscription != null && subscription.bytesUsed > 0L) { // SIP008 & Open Online Config groupTraffic.isVisible = true groupTraffic.text = if (subscription.bytesRemaining > 0L) { - getString( + app.getString( R.string.subscription_traffic, Formatter.formatFileSize( - context, subscription.bytesUsed + app, subscription.bytesUsed ), Formatter.formatFileSize( - context, subscription.bytesRemaining + app, subscription.bytesRemaining ) ) } else { - getString( + app.getString( R.string.subscription_used, Formatter.formatFileSize( - context, subscription.bytesUsed + app, subscription.bytesUsed ) ) } @@ -460,27 +467,31 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), }.firstOrNull() } - var used: Long = 0 - get("upload=([0-9]+)")?.apply { - used += toLong() - } - get("download=([0-9]+)")?.apply { - used += toLong() - } - val total = get("total=([0-9]+)")?.toLong() ?: 0 - if (used > 0 || total > 0) { - text += getString( - R.string.subscription_traffic, - used.toBytesString(), - (total - used).toBytesString() - ) - } - get("expire=([0-9]+)")?.apply { - text += "\n" - text += getString( - R.string.subscription_expire, - Util.timeStamp2Text(this.toLong() * 1000) - ) + try { + var used: Long = 0 + get("upload=([0-9]+)")?.apply { + used += toLong() + } + get("download=([0-9]+)")?.apply { + used += toLong() + } + val total = get("total=([0-9]+)")?.toLong() ?: 0 + if (used > 0 || total > 0) { + text += getString( + R.string.subscription_traffic, + used.toBytesString(), + (total - used).toBytesString() + ) + } + get("expire=([0-9]+)")?.apply { + text += "\n" + text += getString( + R.string.subscription_expire, + Util.timeStamp2Text(this.toLong() * 1000) + ) + } + } catch (_: NumberFormatException) { + // ignore } if (text.isNotEmpty()) { @@ -506,6 +517,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), groupStatus.text = getString(R.string.group_status_proxies, size) } } + GroupType.SUBSCRIPTION -> { groupStatus.text = if (size == 0L) { getString(R.string.group_status_empty_subscription) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt index cf27dd053..e8440bf1c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt @@ -29,7 +29,6 @@ import io.nekohasekai.sagernet.ktx.onMainDispatcher import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.widget.ListListener import io.nekohasekai.sagernet.widget.OutboundPreference -import io.nekohasekai.sagernet.widget.UserAgentPreference import kotlinx.parcelize.Parcelize import moe.matsuri.nb4a.ui.SimpleMenuPreference @@ -163,13 +162,6 @@ class GroupSettingsActivity( } } - fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) { - } - - fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean { - return false - } - class UnsavedChangesDialogFragment : AlertDialogFragment() { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { setTitle(R.string.unsaved_changes_prompt) @@ -233,9 +225,7 @@ class GroupSettingsActivity( onMainDispatcher { supportFragmentManager.beginTransaction() - .replace(R.id.settings, MyPreferenceFragmentCompat().apply { - activity = this@GroupSettingsActivity - }) + .replace(R.id.settings, MyPreferenceFragmentCompat()) .commit() DataStore.dirty = false @@ -258,7 +248,12 @@ class GroupSettingsActivity( finish() return } - entity.subscription?.subscriptionUserinfo = ""; + val keepUserInfo = (entity.type == GroupType.SUBSCRIPTION && + DataStore.groupType == GroupType.SUBSCRIPTION && + entity.subscription?.link == DataStore.subscriptionLink) + if (!keepUserInfo) { + entity.subscription?.subscriptionUserinfo = ""; + } GroupManager.updateGroup(entity.apply { serialize() }) } @@ -299,12 +294,12 @@ class GroupSettingsActivity( class MyPreferenceFragmentCompat : PreferenceFragmentCompat() { - lateinit var activity: GroupSettingsActivity + var activity: GroupSettingsActivity? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = DataStore.profileCacheStore try { - activity.apply { + activity = (requireActivity() as GroupSettingsActivity).apply { createPreferences(savedInstanceState, rootKey) } } catch (e: Exception) { @@ -321,10 +316,6 @@ class GroupSettingsActivity( super.onViewCreated(view, savedInstanceState) ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener) - - activity.apply { - viewCreated(view, savedInstanceState) - } } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { @@ -339,20 +330,15 @@ class GroupSettingsActivity( } true } + R.id.action_apply -> { runOnDefaultDispatcher { - activity.saveAndExit() + activity?.saveAndExit() } true } - else -> false - } - override fun onDisplayPreferenceDialog(preference: Preference) { - activity.apply { - if (displayPreferenceDialog(preference)) return - } - super.onDisplayPreferenceDialog(preference) + else -> false } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt index d2fd8a9d4..2f276ee9f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt @@ -9,11 +9,14 @@ import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.style.ForegroundColorSpan import android.view.MenuItem import android.view.View -import android.widget.ScrollView +import android.view.ViewGroup import androidx.appcompat.widget.Toolbar +import androidx.core.view.ViewCompat +import androidx.core.view.doOnLayout import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.databinding.LayoutLogcatBinding import io.nekohasekai.sagernet.ktx.* +import io.nekohasekai.sagernet.widget.ListListener import libcore.Libcore import moe.matsuri.nb4a.utils.SendLog @@ -36,20 +39,23 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat), binding.textview.breakStrategy = 0 // simple } + ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener) + reloadSession() - // TODO new logcat } private fun getColorForLine(line: String): ForegroundColorSpan { var color = ForegroundColorSpan(Color.GRAY) when { - line.contains(" INFO[") || line.contains(" [Info]") -> { + line.contains("INFO[") || line.contains(" [Info]") -> { color = ForegroundColorSpan((0xFF86C166).toInt()) } - line.contains(" ERROR[") || line.contains(" [Error]") -> { + + line.contains("ERROR[") || line.contains(" [Error]") -> { color = ForegroundColorSpan(Color.RED) } - line.contains(" WARN[") || line.contains(" [Warning]") -> { + + line.contains("WARN[") || line.contains(" [Warning]") -> { color = ForegroundColorSpan(Color.RED) } } @@ -69,9 +75,10 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat), offset += line.length + 1 } binding.textview.text = span - - binding.scroolview.post { - binding.scroolview.fullScroll(ScrollView.FOCUS_DOWN) + binding.textview.clearFocus() + // 等 textview 完成最终 layout 再滚动到底部 + binding.textview.doOnLayout { + binding.scroolview.scrollTo(0, binding.textview.height) } } @@ -94,12 +101,14 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat), } } + R.id.action_send_logcat -> { val context = requireContext() runOnDefaultDispatcher { SendLog.sendLog(context, "NB4A") } } + R.id.action_refresh -> { reloadSession() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt index 1954d1517..a60012e10 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt @@ -10,22 +10,29 @@ import android.os.Bundle import android.os.RemoteException import android.view.KeyEvent import android.view.MenuItem -import android.widget.Toast +import androidx.activity.addCallback import androidx.annotation.IdRes import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.core.view.ViewCompat import androidx.preference.PreferenceDataStore import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar -import io.nekohasekai.sagernet.* +import io.nekohasekai.sagernet.BuildConfig +import io.nekohasekai.sagernet.GroupType +import io.nekohasekai.sagernet.Key +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.aidl.ISagerNetService import io.nekohasekai.sagernet.aidl.SpeedDisplayData import io.nekohasekai.sagernet.aidl.TrafficData import io.nekohasekai.sagernet.bg.BaseService import io.nekohasekai.sagernet.bg.SagerConnection -import io.nekohasekai.sagernet.database.* +import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.database.GroupManager +import io.nekohasekai.sagernet.database.ProfileManager +import io.nekohasekai.sagernet.database.ProxyGroup +import io.nekohasekai.sagernet.database.SubscriptionBean import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener import io.nekohasekai.sagernet.databinding.LayoutMainBinding import io.nekohasekai.sagernet.fmt.AbstractBean @@ -33,12 +40,15 @@ import io.nekohasekai.sagernet.fmt.KryoConverters import io.nekohasekai.sagernet.fmt.PluginEntry import io.nekohasekai.sagernet.group.GroupInterfaceAdapter import io.nekohasekai.sagernet.group.GroupUpdater -import io.nekohasekai.sagernet.ktx.* -import io.nekohasekai.sagernet.widget.ListHolderListener -import libcore.Libcore +import io.nekohasekai.sagernet.ktx.alert +import io.nekohasekai.sagernet.ktx.isPlay +import io.nekohasekai.sagernet.ktx.isPreview +import io.nekohasekai.sagernet.ktx.launchCustomTab +import io.nekohasekai.sagernet.ktx.onMainDispatcher +import io.nekohasekai.sagernet.ktx.parseProxies +import io.nekohasekai.sagernet.ktx.readableMessage +import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import moe.matsuri.nb4a.utils.Util -import java.text.SimpleDateFormat -import java.util.* class MainActivity : ThemedActivity(), SagerConnection.Callback, @@ -68,6 +78,13 @@ class MainActivity : ThemedActivity(), if (savedInstanceState == null) { displayFragmentWithId(R.id.nav_configuration) } + onBackPressedDispatcher.addCallback { + if (supportFragmentManager.findFragmentById(R.id.fragment_holder) is ConfigurationFragment) { + moveTaskToBack(true) + } else { + displayFragmentWithId(R.id.nav_configuration) + } + } binding.fab.setOnClickListener { if (DataStore.serviceState.canStop) SagerNet.stopService() else connect.launch( @@ -77,7 +94,6 @@ class MainActivity : ThemedActivity(), binding.stats.setOnClickListener { if (DataStore.serviceState.connected) binding.stats.testConnection() } setContentView(binding.root) - ViewCompat.setOnApplyWindowInsetsListener(binding.coordinator, ListHolderListener) changeState(BaseService.State.Idle) connection.connect(this, this) DataStore.configurationStore.registerChangeListener(this) @@ -87,6 +103,8 @@ class MainActivity : ThemedActivity(), onNewIntent(intent) } + refreshNavMenu(DataStore.enableClashAPI) + // sdk 33 notification if (Build.VERSION.SDK_INT >= 33) { val checkPermission = @@ -94,10 +112,25 @@ class MainActivity : ThemedActivity(), if (checkPermission != PackageManager.PERMISSION_GRANTED) { //动态申请 ActivityCompat.requestPermissions( - this@MainActivity, arrayOf(POST_NOTIFICATIONS), 0 + this@MainActivity, arrayOf(POST_NOTIFICATIONS), 0 ) } } + + if (isPreview) { + MaterialAlertDialogBuilder(this) + .setTitle(BuildConfig.PRE_VERSION_NAME) + .setMessage(R.string.preview_version_hint) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + + fun refreshNavMenu(clashApi: Boolean) { + if (::navigation.isInitialized) { + navigation.menu.findItem(R.id.nav_traffic)?.isVisible = clashApi + navigation.menu.findItem(R.id.nav_tuiguang)?.isVisible = !isPlay + } } override fun onNewIntent(intent: Intent) { @@ -237,7 +270,7 @@ class MainActivity : ThemedActivity(), } .setNeutralButton(android.R.string.cancel, null) .setNeutralButton(R.string.action_learn_more) { _, _ -> - launchCustomTab("https://matsuridayo.github.io/m-plugin/") + launchCustomTab("https://matsuridayo.github.io/nb4a-plugin/") } .show() } @@ -300,6 +333,7 @@ class MainActivity : ThemedActivity(), R.id.nav_configuration -> { displayFragment(ConfigurationFragment()) } + R.id.nav_group -> displayFragment(GroupFragment()) R.id.nav_route -> displayFragment(RouteFragment()) R.id.nav_settings -> displayFragment(SettingsFragment()) @@ -310,30 +344,19 @@ class MainActivity : ThemedActivity(), launchCustomTab("https://matsuridayo.github.io/") return false } + R.id.nav_about -> displayFragment(AboutFragment()) R.id.nav_tuiguang -> { - launchCustomTab("https://matsuricom.github.io/") + launchCustomTab("https://neko-box.pages.dev/喵") return false } + else -> return false } navigation.menu.findItem(id).isChecked = true return true } - @SuppressLint("CommitTransaction") - fun ruleCreated() { - navigation.menu.findItem(R.id.nav_route).isChecked = true - supportFragmentManager.beginTransaction() - .replace(R.id.fragment_holder, RouteFragment()) - .commitAllowingStateLoss() - if (DataStore.serviceState.started) { - snackbar(getString(R.string.need_reload)).setAction(R.string.apply) { - SagerNet.reloadService() - }.show() - } - } - private fun changeState( state: BaseService.State, msg: String? = null, @@ -344,16 +367,6 @@ class MainActivity : ThemedActivity(), binding.fab.changeState(state, DataStore.serviceState, animate) binding.stats.changeState(state) if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show() - - when (state) { - BaseService.State.Stopped -> { - runOnDefaultDispatcher { - // refresh view - ProfileManager.postUpdate(DataStore.currentProfile) - } - } - else -> {} - } } override fun snackbarInternal(text: CharSequence): Snackbar { @@ -369,18 +382,6 @@ class MainActivity : ThemedActivity(), changeState(state, msg, true) } - override fun routeAlert(type: Int, routeName: String) { - when (type) { - 0 -> { - // need vpn - - Toast.makeText( - this, getString(R.string.route_need_vpn, routeName), Toast.LENGTH_SHORT - ).show() - } - } - } - val connection = SagerConnection(SagerConnection.CONNECTION_ID_MAIN_ACTIVITY_FOREGROUND, true) override fun onServiceConnected(service: ISagerNetService) = changeState( try { @@ -412,6 +413,16 @@ class MainActivity : ThemedActivity(), } } + override fun cbSelectorUpdate(id: Long) { + val old = DataStore.selectedProxy + DataStore.selectedProxy = id + DataStore.currentProfile = id + runOnDefaultDispatcher { + ProfileManager.postUpdate(old, true) + ProfileManager.postUpdate(id, true) + } + } + override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { when (key) { Key.SERVICE_MODE -> onBinderDied() @@ -449,6 +460,7 @@ class MainActivity : ThemedActivity(), binding.drawerLayout.open() navigation.requestFocus() } + KeyEvent.KEYCODE_DPAD_RIGHT -> { if (binding.drawerLayout.isOpen) { binding.drawerLayout.close() @@ -465,42 +477,4 @@ class MainActivity : ThemedActivity(), return fragment != null && fragment.onKeyDown(keyCode, event) } - @SuppressLint("SimpleDateFormat") - override fun onResume() { - super.onResume() - - val sdf = SimpleDateFormat("yyyy-MM-dd") - val now = System.currentTimeMillis() - val expire = Libcore.getExpireTime() * 1000 - val dateExpire = Date(expire) - val build = Libcore.getBuildTime() * 1000 - val dateBuild = Date(build) - - var text: String? = null - if (now > expire) { - text = getString( - R.string.please_update_force, sdf.format(dateBuild), sdf.format(dateExpire) - ) - } else if (now > (expire - 2592000000)) { - // 30 days remind :D - text = getString( - R.string.please_update, sdf.format(dateBuild), sdf.format(dateExpire) - ) - } - - - if (text != null) { - MaterialAlertDialogBuilder(this@MainActivity).setTitle(R.string.insecure) - .setMessage(text) - .setPositiveButton(R.string.action_download) { _, _ -> - launchCustomTab( - "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases" - ) - } - .setNegativeButton(android.R.string.cancel, null) - .setCancelable(false) - .show() - } - } - } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt index 6ef29c114..b8bb94145 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt @@ -3,18 +3,9 @@ package io.nekohasekai.sagernet.ui import android.content.Intent import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.database.ProfileManager import io.nekohasekai.sagernet.databinding.LayoutNetworkBinding -import io.nekohasekai.sagernet.databinding.LayoutProgressBinding -import io.nekohasekai.sagernet.ktx.* -import io.nekohasekai.sagernet.utils.Cloudflare -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.runBlocking +import io.nekohasekai.sagernet.ktx.app class NetworkFragment : NamedFragment(R.layout.layout_network) { @@ -27,58 +18,6 @@ class NetworkFragment : NamedFragment(R.layout.layout_network) { binding.stunTest.setOnClickListener { startActivity(Intent(requireContext(), StunActivity::class.java)) } - - //Markwon.create(requireContext()) - // .setMarkdown(binding.wrapLicense, getString(R.string.warp_license)) - - binding.warpGenerate.setOnClickListener { - runBlocking { - generateWarpConfiguration() - } - } - } - - suspend fun generateWarpConfiguration() { - val activity = requireActivity() as MainActivity - val binding = LayoutProgressBinding.inflate(layoutInflater).apply { - content.setText(R.string.generating) - } - var job: Job? = null - val dialog = AlertDialog.Builder(requireContext()) - .setView(binding.root) - .setCancelable(false) - .setNegativeButton(android.R.string.cancel) { _, _ -> - job?.cancel() - } - .show() - job = runOnDefaultDispatcher { - try { - val bean = Cloudflare.makeWireGuardConfiguration() - if (isActive) { - val groupId = DataStore.selectedGroupForImport() - if (DataStore.selectedGroup != groupId) { - DataStore.selectedGroup = groupId - } - onMainDispatcher { - activity.displayFragmentWithId(R.id.nav_configuration) - } - delay(1000L) - onMainDispatcher { - dialog.dismiss() - } - ProfileManager.createProfile(groupId, bean) - } - } catch (e: Exception) { - Logs.w(e) - onMainDispatcher { - if (isActive) { - dialog.dismiss() - activity.snackbar(e.readableMessage).show() - } - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt index 312a1226a..9fcf93e4a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt @@ -18,7 +18,7 @@ import io.nekohasekai.sagernet.database.SagerDatabase import io.nekohasekai.sagernet.databinding.LayoutEmptyRouteBinding import io.nekohasekai.sagernet.databinding.LayoutRouteItemBinding import io.nekohasekai.sagernet.ktx.* -import io.nekohasekai.sagernet.widget.ListHolderListener +import io.nekohasekai.sagernet.widget.ListListener import io.nekohasekai.sagernet.widget.UndoSnackbarManager class RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItemClickListener { @@ -33,7 +33,7 @@ class RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItem activity = requireActivity() as MainActivity - ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener) + ViewCompat.setOnApplyWindowInsetsListener(view, ListListener) toolbar.setTitle(R.string.menu_route) toolbar.inflateMenu(R.menu.add_route_menu) toolbar.setOnMenuItemClickListener(this) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt index 85ef9a778..aaa062de3 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt @@ -39,6 +39,7 @@ import io.nekohasekai.sagernet.widget.AppListPreference import io.nekohasekai.sagernet.widget.ListListener import io.nekohasekai.sagernet.widget.OutboundPreference import kotlinx.parcelize.Parcelize +import moe.matsuri.nb4a.ui.EditConfigPreference @Suppress("UNCHECKED_CAST") class RouteSettingsActivity( @@ -49,7 +50,7 @@ class RouteSettingsActivity( fun init(packageName: String?) { RuleEntity().apply { if (!packageName.isNullOrBlank()) { - packages = listOf(packageName) + packages = setOf(packageName) name = app.getString(R.string.route_for, PackageCache.loadLabel(packageName)) } }.init() @@ -57,6 +58,7 @@ class RouteSettingsActivity( fun RuleEntity.init() { DataStore.routeName = name + DataStore.serverConfig = config DataStore.routeDomain = domains DataStore.routeIP = ip DataStore.routePort = port @@ -76,6 +78,7 @@ class RouteSettingsActivity( fun RuleEntity.serialize() { name = DataStore.routeName + config = DataStore.serverConfig domains = DataStore.routeDomain ip = DataStore.routeIP port = DataStore.routePort @@ -89,19 +92,17 @@ class RouteSettingsActivity( 2 -> -2L else -> DataStore.routeOutboundRule } - packages = DataStore.routePackages.split("\n").filter { it.isNotBlank() } + packages = DataStore.routePackages.split("\n").filter { it.isNotBlank() }.toSet() if (DataStore.editingId == 0L) { enabled = true } } + private lateinit var editConfigPreference: EditConfigPreference + fun needSave(): Boolean { - if (!DataStore.dirty) return false - if (DataStore.routePackages.isBlank() && DataStore.routeDomain.isBlank() && DataStore.routeIP.isBlank() && DataStore.routePort.isBlank() && DataStore.routeSourcePort.isBlank() && DataStore.routeNetwork.isBlank() && DataStore.routeSource.isBlank() && DataStore.routeProtocol.isBlank()) { - return false - } - return true + return DataStore.dirty } fun PreferenceFragmentCompat.createPreferences( @@ -109,6 +110,16 @@ class RouteSettingsActivity( rootKey: String?, ) { addPreferencesFromResource(R.xml.route_preferences) + + editConfigPreference = findPreference(Key.SERVER_CONFIG)!! + } + + override fun onResume() { + super.onResume() + + if (::editConfigPreference.isInitialized) { + editConfigPreference.notifyChanged() + } } val selectProfileForAdd = registerForActivityResult( @@ -163,7 +174,7 @@ class RouteSettingsActivity( } } - fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean { + fun displayPreferenceDialog(preference: Preference): Boolean { return false } @@ -230,9 +241,7 @@ class RouteSettingsActivity( onMainDispatcher { supportFragmentManager.beginTransaction() - .replace(R.id.settings, MyPreferenceFragmentCompat().apply { - activity = this@RouteSettingsActivity - }) + .replace(R.id.settings, MyPreferenceFragmentCompat()) .commit() DataStore.dirty = false @@ -309,12 +318,12 @@ class RouteSettingsActivity( class MyPreferenceFragmentCompat : PreferenceFragmentCompat() { - lateinit var activity: RouteSettingsActivity + var activity: RouteSettingsActivity? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = DataStore.profileCacheStore try { - activity.apply { + activity = (requireActivity() as RouteSettingsActivity).apply { createPreferences(savedInstanceState, rootKey) } } catch (e: Exception) { @@ -332,7 +341,7 @@ class RouteSettingsActivity( ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener) - activity.apply { + activity?.apply { viewCreated(view, savedInstanceState) } } @@ -349,17 +358,19 @@ class RouteSettingsActivity( } true } + R.id.action_apply -> { runOnDefaultDispatcher { - activity.saveAndExit() + activity?.saveAndExit() } true } + else -> false } override fun onDisplayPreferenceDialog(preference: Preference) { - activity.apply { + activity?.apply { if (displayPreferenceDialog(preference)) return } super.onDisplayPreferenceDialog(preference) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt index c8477f599..99f6f6468 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt @@ -1,12 +1,9 @@ package io.nekohasekai.sagernet.ui import android.Manifest -import android.annotation.SuppressLint import android.content.Intent -import android.content.pm.ActivityInfo import android.content.pm.ShortcutManager import android.graphics.ImageDecoder -import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore @@ -15,6 +12,7 @@ import android.view.MenuItem import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.getSystemService +import androidx.core.net.toUri import com.google.zxing.Result import com.king.zxing.CameraScan import com.king.zxing.DefaultCameraScan @@ -28,7 +26,6 @@ import io.nekohasekai.sagernet.database.ProfileManager import io.nekohasekai.sagernet.databinding.LayoutScannerBinding import io.nekohasekai.sagernet.group.RawUpdater import io.nekohasekai.sagernet.ktx.* -import io.nekohasekai.sagernet.widget.ListHolderListener import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger @@ -39,16 +36,12 @@ class ScannerActivity : ThemedActivity(), lateinit var binding: LayoutScannerBinding lateinit var cameraScan: CameraScan - @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } + if (Build.VERSION.SDK_INT >= 25) getSystemService()!!.reportShortcutUsed("scan") binding = LayoutScannerBinding.inflate(layoutInflater) setContentView(binding.root) - ListHolderListener.setup(this) setSupportActionBar(findViewById(R.id.toolbar)) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) @@ -145,7 +138,7 @@ class ScannerActivity : ThemedActivity(), } catch (e: SubscriptionFoundException) { startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply { action = Intent.ACTION_VIEW - data = Uri.parse(e.link) + data = e.link.toUri() }) } catch (e: Throwable) { Logs.w(e) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt index 0bc105a5b..c50f1274f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt @@ -4,14 +4,14 @@ import android.os.Bundle import android.view.View import androidx.core.view.ViewCompat import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.widget.ListHolderListener +import io.nekohasekai.sagernet.widget.ListListener class SettingsFragment : ToolbarFragment(R.layout.layout_config_settings) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener) + ViewCompat.setOnApplyWindowInsetsListener(view, ListListener) toolbar.setTitle(R.string.settings) parentFragmentManager.beginTransaction() diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt index 118b1760d..18d645455 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt @@ -16,14 +16,14 @@ import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.utils.Theme -import io.nekohasekai.sagernet.widget.AppListPreference -import moe.matsuri.nb4a.Protocols import moe.matsuri.nb4a.ui.* class SettingsPreferenceFragment : PreferenceFragmentCompat() { private lateinit var isProxyApps: SwitchPreference - private lateinit var nekoPlugins: AppListPreference + + private lateinit var globalCustomConfig: EditConfigPreference + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -41,16 +41,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { DataStore.initGlobal() addPreferencesFromResource(R.xml.global_preferences) - DataStore.routePackages = DataStore.nekoPlugins - nekoPlugins = findPreference(Key.NEKO_PLUGIN_MANAGED)!! - nekoPlugins.setOnPreferenceClickListener { - // borrow from route app settings - startActivity(Intent( - context, AppListActivity::class.java - ).apply { putExtra(Key.NEKO_PLUGIN_MANAGED, true) }) - true - } - val appTheme = findPreference(Key.APP_THEME)!! appTheme.setOnPreferenceChangeListener { _, newTheme -> if (DataStore.serviceState.started) { @@ -76,42 +66,22 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { val allowAccess = findPreference(Key.ALLOW_ACCESS)!! val appendHttpProxy = findPreference(Key.APPEND_HTTP_PROXY)!! - val portLocalDns = findPreference(Key.LOCAL_DNS_PORT)!! val showDirectSpeed = findPreference(Key.SHOW_DIRECT_SPEED)!! val ipv6Mode = findPreference(Key.IPV6_MODE)!! -// val domainStrategy = findPreference(Key.DOMAIN_STRATEGY)!! val trafficSniffing = findPreference(Key.TRAFFIC_SNIFFING)!! - val muxConcurrency = findPreference(Key.MUX_CONCURRENCY)!! - val tcpKeepAliveInterval = findPreference(Key.TCP_KEEP_ALIVE_INTERVAL)!! - tcpKeepAliveInterval.isVisible = false - val bypassLan = findPreference(Key.BYPASS_LAN)!! val bypassLanInCore = findPreference(Key.BYPASS_LAN_IN_CORE)!! val remoteDns = findPreference(Key.REMOTE_DNS)!! val directDns = findPreference(Key.DIRECT_DNS)!! - val directDnsUseSystem = findPreference(Key.DIRECT_DNS_USE_SYSTEM)!! val enableDnsRouting = findPreference(Key.ENABLE_DNS_ROUTING)!! val enableFakeDns = findPreference(Key.ENABLE_FAKEDNS)!! - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - DataStore.directDnsUseSystem = false - directDnsUseSystem.remove() - } else { - directDns.isEnabled = !directDnsUseSystem.isChecked - directDnsUseSystem.setOnPreferenceChangeListener { _, newValue -> - directDns.isEnabled = !(newValue as Boolean) - needReload() - true - } - } - - val requireTransproxy = findPreference(Key.REQUIRE_TRANSPROXY)!! - val transproxyPort = findPreference(Key.TRANSPROXY_PORT)!! - val transproxyMode = findPreference(Key.TRANSPROXY_MODE)!! val logLevel = findPreference(Key.LOG_LEVEL)!! val mtu = findPreference(Key.MTU)!! + globalCustomConfig = findPreference(Key.GLOBAL_CUSTOM_CONFIG)!! + globalCustomConfig.useConfigStore(Key.GLOBAL_CUSTOM_CONFIG) logLevel.dialogLayoutResource = R.layout.layout_loglevel_help logLevel.setOnPreferenceChangeListener { _, _ -> @@ -140,28 +110,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { true } - transproxyPort.isEnabled = requireTransproxy.isChecked - transproxyMode.isEnabled = requireTransproxy.isChecked - - requireTransproxy.setOnPreferenceChangeListener { _, newValue -> - transproxyPort.isEnabled = newValue as Boolean - transproxyMode.isEnabled = newValue - needReload() - true - } - - val muxProtocols = findPreference(Key.MUX_PROTOCOLS)!! - - muxProtocols.apply { - val e = Protocols.getCanMuxList().toTypedArray() - entries = e - entryValues = e - } - - val dnsNetwork = findPreference(Key.DNS_NETWORK)!! - - portLocalDns.setOnBindEditTextListener(EditTextPreferenceModifiers.Port) - muxConcurrency.setOnBindEditTextListener(EditTextPreferenceModifiers.Port) mixedPort.setOnBindEditTextListener(EditTextPreferenceModifiers.Port) val metedNetwork = findPreference(Key.METERED_NETWORK)!! @@ -185,19 +133,25 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { true } + serviceMode.setOnPreferenceChangeListener { _, _ -> + if (DataStore.serviceState.started) SagerNet.stopService() + true + } + val tunImplementation = findPreference(Key.TUN_IMPLEMENTATION)!! val resolveDestination = findPreference(Key.RESOLVE_DESTINATION)!! val acquireWakeLock = findPreference(Key.ACQUIRE_WAKE_LOCK)!! val enableClashAPI = findPreference(Key.ENABLE_CLASH_API)!! + enableClashAPI.setOnPreferenceChangeListener { _, newValue -> + (activity as MainActivity?)?.refreshNavMenu(newValue as Boolean) + needReload() + true + } - serviceMode.onPreferenceChangeListener = reloadListener mixedPort.onPreferenceChangeListener = reloadListener appendHttpProxy.onPreferenceChangeListener = reloadListener showDirectSpeed.onPreferenceChangeListener = reloadListener -// domainStrategy.onPreferenceChangeListener = reloadListener trafficSniffing.onPreferenceChangeListener = reloadListener - muxConcurrency.onPreferenceChangeListener = reloadListener - tcpKeepAliveInterval.onPreferenceChangeListener = reloadListener bypassLan.onPreferenceChangeListener = reloadListener bypassLanInCore.onPreferenceChangeListener = reloadListener mtu.onPreferenceChangeListener = reloadListener @@ -206,20 +160,14 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { remoteDns.onPreferenceChangeListener = reloadListener directDns.onPreferenceChangeListener = reloadListener enableDnsRouting.onPreferenceChangeListener = reloadListener - dnsNetwork.onPreferenceChangeListener = reloadListener - portLocalDns.onPreferenceChangeListener = reloadListener ipv6Mode.onPreferenceChangeListener = reloadListener allowAccess.onPreferenceChangeListener = reloadListener - transproxyPort.onPreferenceChangeListener = reloadListener - transproxyMode.onPreferenceChangeListener = reloadListener - resolveDestination.onPreferenceChangeListener = reloadListener tunImplementation.onPreferenceChangeListener = reloadListener acquireWakeLock.onPreferenceChangeListener = reloadListener - enableClashAPI.onPreferenceChangeListener = reloadListener - + globalCustomConfig.onPreferenceChangeListener = reloadListener } override fun onResume() { @@ -228,8 +176,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { if (::isProxyApps.isInitialized) { isProxyApps.isChecked = DataStore.proxyApps } - if (::nekoPlugins.isInitialized) { - nekoPlugins.postUpdate() + if (::globalCustomConfig.isInitialized) { + globalCustomConfig.notifyChanged() } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SwitchActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SwitchActivity.kt index 44dddb33a..be205288a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/SwitchActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SwitchActivity.kt @@ -16,7 +16,10 @@ class SwitchActivity : ThemedActivity(R.layout.layout_empty), super.onCreate(savedInstanceState) supportFragmentManager.beginTransaction() - .replace(R.id.fragment_holder, ConfigurationFragment(true, null, R.string.action_switch)) + .replace( + R.id.fragment_holder, + ConfigurationFragment(true, null, R.string.action_switch) + ) .commitAllowingStateLoss() } @@ -24,8 +27,8 @@ class SwitchActivity : ThemedActivity(R.layout.layout_empty), val old = DataStore.selectedProxy DataStore.selectedProxy = profileId runOnMainDispatcher { - ProfileManager.postUpdate(old) - ProfileManager.postUpdate(profileId) + ProfileManager.postUpdate(old, true) + ProfileManager.postUpdate(profileId, true) } SagerNet.reloadService() finish() diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt index 4c57f200e..050c874b7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt @@ -1,12 +1,18 @@ package io.nekohasekai.sagernet.ui import android.content.res.Configuration +import android.os.Build import android.os.Bundle import android.widget.TextView import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar +import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.utils.Theme abstract class ThemedActivity : AppCompatActivity { @@ -28,6 +34,20 @@ abstract class ThemedActivity : AppCompatActivity { super.onCreate(savedInstanceState) uiMode = resources.configuration.uiMode + + if (Build.VERSION.SDK_INT >= 35) { + ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { _, insets -> + val top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top + findViewById(R.id.appbar)?.apply { + updatePadding(top = top) +// Logs.w("appbar $top") + } +// findViewById(R.id.nav_view)?.apply { +// updatePadding(top = top) +// } + insets + } + } } override fun setTheme(resId: Int) { @@ -51,6 +71,7 @@ abstract class ThemedActivity : AppCompatActivity { maxLines = 10 } } + internal open fun snackbarInternal(text: CharSequence): Snackbar = throw NotImplementedError() } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt index bf73ca1d9..32738ea98 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt @@ -7,7 +7,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.databinding.LayoutToolsBinding -import io.nekohasekai.sagernet.ktx.isExpert class ToolsFragment : ToolbarFragment(R.layout.layout_tools) { @@ -19,8 +18,6 @@ class ToolsFragment : ToolbarFragment(R.layout.layout_tools) { tools.add(NetworkFragment()) tools.add(BackupFragment()) - if (isExpert) tools.add(DebugFragment()) - val binding = LayoutToolsBinding.bind(view) binding.toolsPager.adapter = ToolsAdapter(tools) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/VpnRequestActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/VpnRequestActivity.kt index 55a7b33ae..a1d81afc6 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/VpnRequestActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/VpnRequestActivity.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.VpnService +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContract @@ -26,7 +27,15 @@ class VpnRequestActivity : AppCompatActivity() { super.onCreate(savedInstanceState) if (getSystemService()!!.isKeyguardLocked) { receiver = broadcastReceiver { _, _ -> connect.launch(null) } - registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT)) + if (SDK_INT >= 33) { + registerReceiver( + receiver, + IntentFilter(Intent.ACTION_USER_PRESENT), + Context.RECEIVER_EXPORTED + ) + } else { + registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT)) + } } else connect.launch(null) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt index 9f4274043..e144a26ef 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt @@ -5,7 +5,12 @@ import android.content.DialogInterface import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.ViewGroup.MarginLayoutParams +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams import androidx.core.widget.addTextChangedListener import com.blacksquircle.ui.editorkit.insert import com.blacksquircle.ui.language.json.JsonLanguage @@ -16,8 +21,11 @@ import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.databinding.LayoutEditConfigBinding -import io.nekohasekai.sagernet.ktx.* +import io.nekohasekai.sagernet.ktx.getColorAttr +import io.nekohasekai.sagernet.ktx.readableMessage +import io.nekohasekai.sagernet.ktx.toStringPretty import io.nekohasekai.sagernet.ui.ThemedActivity +import io.nekohasekai.sagernet.widget.ListListener import moe.matsuri.nb4a.ui.ExtendedKeyboard import org.json.JSONObject @@ -25,6 +33,7 @@ class ConfigEditActivity : ThemedActivity() { var dirty = false var key = Key.SERVER_CONFIG + var useConfigStore = false class UnsavedChangesDialogFragment : AlertDialogFragment() { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { @@ -47,6 +56,7 @@ class ConfigEditActivity : ThemedActivity() { intent?.extras?.apply { getString("key")?.let { key = it } + getString("useConfigStore")?.let { useConfigStore = true } } binding = LayoutEditConfigBinding.inflate(layoutInflater) @@ -62,7 +72,11 @@ class ConfigEditActivity : ThemedActivity() { binding.editor.apply { language = JsonLanguage() setHorizontallyScrolling(true) - setTextContent(DataStore.profileCacheStore.getString(key)!!) + if (useConfigStore) { + setTextContent(DataStore.configurationStore.getString(key) ?: "") + } else { + setTextContent(DataStore.profileCacheStore.getString(key) ?: "") + } addTextChangedListener { if (!dirty) { dirty = true @@ -72,7 +86,10 @@ class ConfigEditActivity : ThemedActivity() { } binding.actionTab.setOnClickListener { - binding.editor.insert(binding.editor.tab()) + try { + binding.editor.insert(binding.editor.tab()) + } catch (e: Exception) { + } } binding.actionUndo.setOnClickListener { try { @@ -93,10 +110,34 @@ class ConfigEditActivity : ThemedActivity() { } val extendedKeyboard = findViewById(R.id.extended_keyboard) - extendedKeyboard.setKeyListener { char -> binding.editor.insert(char) } + extendedKeyboard.setKeyListener { char -> + try { + binding.editor.insert(char) + } catch (e: Exception) { + } + } extendedKeyboard.setHasFixedSize(true) extendedKeyboard.submitList("{},:_\"".map { it.toString() }) extendedKeyboard.setBackgroundColor(getColorAttr(R.attr.primaryOrTextPrimary)) + + val keyboardContainer = findViewById(R.id.keyboard_container) + ViewCompat.setOnApplyWindowInsetsListener(keyboardContainer) { v, windowInsets -> + val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) + val systemBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime()) + v.updateLayoutParams { + // systemBar insets are applied to the bottom of the keyboard + if (imeVisible) { + bottomMargin = imeInsets.bottom - systemBarInsets.bottom + } else { + bottomMargin = 0 + } + } + + WindowInsetsCompat.CONSUMED + } + + ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener) } fun formatText(): String? { @@ -115,7 +156,11 @@ class ConfigEditActivity : ThemedActivity() { fun saveAndExit() { formatText()?.let { - DataStore.profileCacheStore.putString(key, it) + if (useConfigStore) { + DataStore.configurationStore.putString(key, it) + } else { + DataStore.profileCacheStore.putString(key, it) + } finish() } } 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 index 02534b593..82bef5933 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt @@ -3,6 +3,7 @@ package io.nekohasekai.sagernet.ui.profile import android.os.Bundle import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore @@ -17,11 +18,12 @@ class HysteriaSettingsActivity : ProfileSettingsActivity() { override fun HysteriaBean.init() { DataStore.profileName = name + DataStore.protocolVersion = protocolVersion DataStore.serverAddress = serverAddress - DataStore.serverPort = serverPort + DataStore.serverPorts = serverPorts DataStore.serverObfs = obfuscation DataStore.serverAuthType = authPayloadType - DataStore.serverProtocolVersion = protocol + DataStore.serverProtocolInt = protocol DataStore.serverPassword = authPayload DataStore.serverSNI = sni DataStore.serverALPN = alpn @@ -37,12 +39,13 @@ class HysteriaSettingsActivity : ProfileSettingsActivity() { override fun HysteriaBean.serialize() { name = DataStore.profileName + protocolVersion = DataStore.protocolVersion serverAddress = DataStore.serverAddress - serverPort = DataStore.serverPort + serverPorts = DataStore.serverPorts obfuscation = DataStore.serverObfs authPayloadType = DataStore.serverAuthType authPayload = DataStore.serverPassword - protocol = DataStore.serverProtocolVersion + protocol = DataStore.serverProtocolInt sni = DataStore.serverSNI alpn = DataStore.serverALPN caText = DataStore.serverCertificates @@ -69,6 +72,47 @@ class HysteriaSettingsActivity : ProfileSettingsActivity() { true } + val protocol = findPreference(Key.SERVER_PROTOCOL)!! + val alpn = findPreference(Key.SERVER_ALPN)!! + + fun updateVersion(v: Int) { + if (v == 2) { + authPayload.isVisible = true + // + authType.isVisible = false + protocol.isVisible = false + alpn.isVisible = false + // + findPreference(Key.SERVER_STREAM_RECEIVE_WINDOW)!!.isVisible = + false + findPreference(Key.SERVER_CONNECTION_RECEIVE_WINDOW)!!.isVisible = + false + findPreference(Key.SERVER_DISABLE_MTU_DISCOVERY)!!.isVisible = + false + // + authPayload.title = resources.getString(R.string.password) + } else { + authType.isVisible = true + authPayload.isVisible = true + protocol.isVisible = true + alpn.isVisible = true + // + findPreference(Key.SERVER_STREAM_RECEIVE_WINDOW)!!.isVisible = + true + findPreference(Key.SERVER_CONNECTION_RECEIVE_WINDOW)!!.isVisible = + true + findPreference(Key.SERVER_DISABLE_MTU_DISCOVERY)!!.isVisible = + true + // + authPayload.title = resources.getString(R.string.hysteria_auth_payload) + } + } + findPreference(Key.PROTOCOL_VERSION)!!.setOnPreferenceChangeListener { _, newValue -> + updateVersion(newValue.toString().toIntOrNull() ?: 1) + true + } + updateVersion(DataStore.protocolVersion) + findPreference(Key.SERVER_UPLOAD_SPEED)!!.apply { setOnBindEditTextListener(EditTextPreferenceModifiers.Number) } @@ -82,13 +126,12 @@ class HysteriaSettingsActivity : ProfileSettingsActivity() { setOnBindEditTextListener(EditTextPreferenceModifiers.Number) } - findPreference(Key.SERVER_PORT)!!.apply { - setOnBindEditTextListener(EditTextPreferenceModifiers.Port) - } - findPreference(Key.SERVER_PASSWORD)!!.apply { summaryProvider = PasswordSummaryProvider } + findPreference(Key.SERVER_OBFS)!!.apply { + summaryProvider = PasswordSummaryProvider + } findPreference(Key.SERVER_HOP_INTERVAL)!!.apply { setOnBindEditTextListener(EditTextPreferenceModifiers.Number) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt new file mode 100644 index 000000000..41c12cf7b --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt @@ -0,0 +1,77 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * * + * 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 androidx.preference.PreferenceFragmentCompat +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.mieru.MieruBean +import io.nekohasekai.sagernet.ktx.applyDefaultValues +import moe.matsuri.nb4a.ui.SimpleMenuPreference + +class MieruSettingsActivity : ProfileSettingsActivity() { + + override fun createEntity() = MieruBean().applyDefaultValues() + + override fun MieruBean.init() { + DataStore.profileName = name + DataStore.serverAddress = serverAddress + DataStore.serverPort = serverPort + DataStore.serverProtocol = protocol + DataStore.serverUsername = username + DataStore.serverPassword = password + DataStore.serverMTU = mtu + } + + override fun MieruBean.serialize() { + name = DataStore.profileName + serverAddress = DataStore.serverAddress + serverPort = DataStore.serverPort + protocol = DataStore.serverProtocol + username = DataStore.serverUsername + password = DataStore.serverPassword + mtu = DataStore.serverMTU + } + + override fun PreferenceFragmentCompat.createPreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + addPreferencesFromResource(R.xml.mieru_preferences) + findPreference(Key.SERVER_PORT)!!.apply { + setOnBindEditTextListener(EditTextPreferenceModifiers.Port) + } + findPreference(Key.SERVER_PASSWORD)!!.apply { + summaryProvider = PasswordSummaryProvider + } + val protocol = findPreference(Key.SERVER_PROTOCOL)!! + val mtu = findPreference(Key.SERVER_MTU)!! + mtu.isVisible = protocol.value.equals("UDP") + protocol.setOnPreferenceChangeListener { _, newValue -> + mtu.isVisible = newValue.equals("UDP") + true + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/NaiveSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/NaiveSettingsActivity.kt index 0c0068f3a..88dcaaf3c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/NaiveSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/NaiveSettingsActivity.kt @@ -24,6 +24,7 @@ class NaiveSettingsActivity : ProfileSettingsActivity() { DataStore.serverCertificates = certificates DataStore.serverHeaders = extraHeaders DataStore.serverInsecureConcurrency = insecureConcurrency + DataStore.profileCacheStore.putBoolean("sUoT", sUoT) } override fun NaiveBean.serialize() { @@ -37,6 +38,7 @@ class NaiveSettingsActivity : ProfileSettingsActivity() { certificates = DataStore.serverCertificates extraHeaders = DataStore.serverHeaders.replace("\r\n", "\n") insecureConcurrency = DataStore.serverInsecureConcurrency + sUoT = DataStore.profileCacheStore.getBoolean("sUoT") } override fun PreferenceFragmentCompat.createPreferences( diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt index 596c34bf1..6424502ba 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt @@ -120,9 +120,8 @@ abstract class ProfileSettingsActivity( onMainDispatcher { supportFragmentManager.beginTransaction() - .replace(R.id.settings, MyPreferenceFragmentCompat().apply { - activity = this@ProfileSettingsActivity - }).commit() + .replace(R.id.settings, MyPreferenceFragmentCompat()) + .commit() } } @@ -210,12 +209,12 @@ abstract class ProfileSettingsActivity( class MyPreferenceFragmentCompat : PreferenceFragmentCompat() { - lateinit var activity: ProfileSettingsActivity<*> + var activity: ProfileSettingsActivity<*>? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = DataStore.profileCacheStore try { - activity.apply { + activity = (requireActivity() as ProfileSettingsActivity<*>).apply { createPreferences(savedInstanceState, rootKey) } } catch (e: Exception) { @@ -233,12 +232,11 @@ abstract class ProfileSettingsActivity( ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener) - activity.apply { + activity?.apply { viewCreated(view, savedInstanceState) + DataStore.dirty = false + DataStore.profileCacheStore.registerChangeListener(this) } - - DataStore.dirty = false - DataStore.profileCacheStore.registerChangeListener(activity) } var callbackCustom: ((String) -> Unit)? = null @@ -273,14 +271,16 @@ abstract class ProfileSettingsActivity( } true } + R.id.action_apply -> { runOnDefaultDispatcher { - activity.saveAndExit() + activity?.saveAndExit() } true } + R.id.action_custom_outbound_json -> { - activity.proxyEntity?.apply { + activity?.proxyEntity?.apply { val bean = requireBean() DataStore.serverCustomOutbound = bean.customOutboundJson callbackCustomOutbound = { bean.customOutboundJson = it } @@ -294,8 +294,9 @@ abstract class ProfileSettingsActivity( } true } + R.id.action_custom_config_json -> { - activity.proxyEntity?.apply { + activity?.proxyEntity?.apply { val bean = requireBean() DataStore.serverCustom = bean.customConfigJson callbackCustom = { bean.customConfigJson = it } @@ -309,7 +310,9 @@ abstract class ProfileSettingsActivity( } true } + R.id.action_create_shortcut -> { + val activity = requireActivity() as ProfileSettingsActivity<*> val ent = activity.proxyEntity!! val shortcut = ShortcutInfoCompat.Builder(activity, "shortcut-profile-${ent.id}") .setShortLabel(ent.displayName()) @@ -326,7 +329,9 @@ abstract class ProfileSettingsActivity( }).build() ShortcutManagerCompat.requestPinShortcut(activity, shortcut, null) } + R.id.action_move -> { + val activity = requireActivity() as ProfileSettingsActivity<*> val view = LinearLayout(context).apply { val ent = activity.proxyEntity!! orientation = LinearLayout.VERTICAL @@ -362,11 +367,12 @@ abstract class ProfileSettingsActivity( MaterialAlertDialogBuilder(activity).setView(scrollView).show() true } + else -> false } override fun onDisplayPreferenceDialog(preference: Preference) { - activity.apply { + activity?.apply { if (displayPreferenceDialog(preference)) return } super.onDisplayPreferenceDialog(preference) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt index cdf3ccd20..6b3bb850f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt @@ -39,7 +39,7 @@ class ShadowsocksSettingsActivity : ProfileSettingsActivity() { val pn = pluginName.readStringFromCache() val pc = pluginConfig.readStringFromCache() - plugin = if (pn.isNotBlank() && pc.isNotBlank()) "$pn;$pc" else "" + plugin = if (pn.isNotBlank()) "$pn;$pc" else "" } override fun PreferenceFragmentCompat.createPreferences( diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt index 3d5d8e333..11005ed13 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt @@ -18,9 +18,11 @@ class SocksSettingsActivity : ProfileSettingsActivity() { DataStore.serverAddress = serverAddress DataStore.serverPort = serverPort - DataStore.serverProtocolVersion = protocol + DataStore.serverProtocolInt = protocol DataStore.serverUsername = username DataStore.serverPassword = password + + DataStore.profileCacheStore.putBoolean("sUoT", sUoT) } override fun SOCKSBean.serialize() { @@ -28,9 +30,11 @@ class SocksSettingsActivity : ProfileSettingsActivity() { serverAddress = DataStore.serverAddress serverPort = DataStore.serverPort - protocol = DataStore.serverProtocolVersion + protocol = DataStore.serverProtocolInt username = DataStore.serverUsername password = DataStore.serverPassword + + sUoT = DataStore.profileCacheStore.getBoolean("sUoT") } override fun PreferenceFragmentCompat.createPreferences( @@ -50,7 +54,7 @@ class SocksSettingsActivity : ProfileSettingsActivity() { password.isVisible = version == SOCKSBean.PROTOCOL_SOCKS5 } - updateProtocol(DataStore.serverProtocolVersion) + updateProtocol(DataStore.protocolVersion) protocol.setOnPreferenceChangeListener { _, newValue -> updateProtocol((newValue as String).toInt()) true diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/StandardV2RaySettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/StandardV2RaySettingsActivity.kt index 9c242e77b..cc769bbc1 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/StandardV2RaySettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/StandardV2RaySettingsActivity.kt @@ -44,6 +44,14 @@ abstract class StandardV2RaySettingsActivity : ProfileSettingsActivity - updateTle(newValue as String) + updateTls(newValue as String) true } } } - fun updateView(network: String) { + private fun updateView(network: String) { host.preference.isVisible = false path.preference.isVisible = false wsCategory.isVisible = false @@ -146,12 +156,14 @@ abstract class StandardV2RaySettingsActivity : ProfileSettingsActivity { host.preference.setTitle(R.string.http_host) path.preference.setTitle(R.string.http_path) host.preference.isVisible = true path.preference.isVisible = true } + "ws" -> { host.preference.setTitle(R.string.ws_host) path.preference.setTitle(R.string.ws_path) @@ -159,17 +171,26 @@ abstract class StandardV2RaySettingsActivity : ProfileSettingsActivity { path.preference.setTitle(R.string.grpc_service_name) path.preference.isVisible = true } + + "httpupgrade" -> { + host.preference.setTitle(R.string.http_upgrade_host) + path.preference.setTitle(R.string.http_upgrade_path) + host.preference.isVisible = true + path.preference.isVisible = true + } } } - fun updateTle(tle: String) { - val isTLS = tle == "tls" + private fun updateTls(tls: String) { + val isTLS = "tls" in tls securityCategory.isVisible = isTLS tlsCamouflageCategory.isVisible = isTLS + echCategory.isVisible = isTLS } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt index b54849960..dfcf97b99 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt @@ -18,6 +18,7 @@ class TuicSettingsActivity : ProfileSettingsActivity() { DataStore.profileName = name DataStore.serverAddress = serverAddress DataStore.serverPort = serverPort + DataStore.serverUsername = uuid DataStore.serverPassword = token DataStore.serverALPN = alpn DataStore.serverCertificates = caText @@ -26,8 +27,6 @@ class TuicSettingsActivity : ProfileSettingsActivity() { DataStore.serverDisableSNI = disableSNI DataStore.serverSNI = sni DataStore.serverReduceRTT = reduceRTT - DataStore.serverMTU = mtu - DataStore.serverFastConnect = fastConnect DataStore.serverAllowInsecure = allowInsecure } @@ -35,6 +34,7 @@ class TuicSettingsActivity : ProfileSettingsActivity() { name = DataStore.profileName serverAddress = DataStore.serverAddress serverPort = DataStore.serverPort + uuid = DataStore.serverUsername token = DataStore.serverPassword alpn = DataStore.serverALPN caText = DataStore.serverCertificates @@ -43,8 +43,6 @@ class TuicSettingsActivity : ProfileSettingsActivity() { disableSNI = DataStore.serverDisableSNI sni = DataStore.serverSNI reduceRTT = DataStore.serverReduceRTT - mtu = DataStore.serverMTU - fastConnect = DataStore.serverFastConnect allowInsecure = DataStore.serverAllowInsecure } diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt deleted file mode 100644 index eee031f88..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt +++ /dev/null @@ -1,72 +0,0 @@ -package io.nekohasekai.sagernet.utils - -import com.wireguard.crypto.KeyPair -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean -import io.nekohasekai.sagernet.ktx.Logs -import io.nekohasekai.sagernet.utils.cf.DeviceResponse -import io.nekohasekai.sagernet.utils.cf.RegisterRequest -import io.nekohasekai.sagernet.utils.cf.UpdateDeviceRequest -import libcore.Libcore -import moe.matsuri.nb4a.utils.JavaUtil.gson - -// kang from wgcf -object Cloudflare { - - private const val API_URL = "https://api.cloudflareclient.com" - private const val API_VERSION = "v0a1922" - - private const val CLIENT_VERSION_KEY = "CF-Client-Version" - private const val CLIENT_VERSION = "a-6.3-1922" - - fun makeWireGuardConfiguration(): WireGuardBean { - val keyPair = KeyPair() - val client = Libcore.newHttpClient().apply { - pinnedTLS12() - trySocks5(DataStore.mixedPort) - } - - try { - val response = client.newRequest().apply { - setMethod("POST") - setURL("$API_URL/$API_VERSION/reg") - setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION) - setHeader("Accept", "application/json") - setHeader("Content-Type", "application/json") - setContentString(RegisterRequest.newRequest(keyPair.publicKey)) - setUserAgent("okhttp/3.12.1") - }.execute() - - Logs.d(response.contentString) - val device = gson.fromJson(response.contentString, DeviceResponse::class.java) - val accessToken = device.token - - client.newRequest().apply { - setMethod("PATCH") - setURL(API_URL + "/" + API_VERSION + "/reg/" + device.id + "/account/reg/" + device.id) - setHeader("Accept", "application/json") - setHeader("Content-Type", "application/json") - setHeader("Authorization", "Bearer $accessToken") - setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION) - setContentString(UpdateDeviceRequest.newRequest()) - setUserAgent("okhttp/3.12.1") - }.execute() - - val peer = device.config.peers[0] - val localAddresses = device.config.interfaceX.addresses - return WireGuardBean().apply { - name = "CloudFlare Warp ${device.account.id}" - privateKey = keyPair.privateKey.toBase64() - peerPublicKey = peer.publicKey - serverAddress = peer.endpoint.host.substringBeforeLast(":") - serverPort = peer.endpoint.host.substringAfterLast(":").toInt() - localAddress = localAddresses.v4 + "/32" + "\n" + localAddresses.v6 + "/128" - mtu = 1280 - reserved = device.config.clientId - } - } finally { - client.close() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt index 292f929dc..2aa93fac7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt @@ -6,6 +6,7 @@ import android.os.Build import android.util.Log import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig +import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.preference.PublicDatabase import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.app @@ -61,7 +62,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler { fun buildReportHeader(): String { var report = "" - report += "NekoBox for Andoird ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) ${BuildConfig.FLAVOR.uppercase()}\n" + report += "NekoBox for Android ${SagerNet.appVersionNameForDisplay} (${BuildConfig.VERSION_CODE})\n" report += "Date: ${getCurrentMilliSecondUTCTimeStamp()}\n\n" report += "OS_VERSION: ${getSystemPropertyWithAndroidAPI("os.version")}\n" report += "SDK_INT: ${Build.VERSION.SDK_INT}\n" @@ -102,7 +103,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler { report += "\n" report += pair.key + ": " + pair.toString() } - }catch (e: Exception) { + } catch (e: Exception) { report += "Export settings failed: " + formatThrowable(e) } @@ -136,7 +137,8 @@ object CrashHandler : Thread.UncaughtExceptionHandler { if (matcher.matches()) { key = matcher.group(1) value = matcher.group(2) - if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] = value + if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] = + value } } bufferedReader.close() diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/DeviceStorageApp.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/DeviceStorageApp.kt deleted file mode 100644 index fc4f63244..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/DeviceStorageApp.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.nekohasekai.sagernet.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 -} diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt index 136563d7a..c5e9370c7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.matsuri.nb4a.plugin.Plugins +import java.util.concurrent.atomic.AtomicBoolean object PackageCache { @@ -20,9 +21,11 @@ object PackageCache { lateinit var packageMap: Map val uidMap = HashMap>() val loaded = Mutex(true) + var registerd = AtomicBoolean(false) // called from init (suspend) fun register() { + if (registerd.getAndSet(true)) return reload() app.listenForPackageChanges(false) { reload() @@ -48,7 +51,7 @@ object PackageCache { }.associateBy { it.packageName } installedPluginPackages = rawPackageInfo.filter { - Plugins.isExeOrPlugin(it) + Plugins.isExe(it) }.associateBy { it.packageName } val installed = app.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) @@ -64,17 +67,12 @@ object PackageCache { operator fun get(uid: Int) = uidMap[uid] operator fun get(packageName: String) = packageMap[packageName] - suspend fun awaitLoad() { + fun awaitLoadSync() { if (::packageMap.isInitialized) { return } - loaded.withLock { - // just await - } - } - - fun awaitLoadSync() { - if (::packageMap.isInitialized) { + if (!registerd.get()) { + register() return } runBlocking { diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt deleted file mode 100644 index 874304dc2..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt +++ /dev/null @@ -1,114 +0,0 @@ -package io.nekohasekai.sagernet.utils.cf - - -import com.google.gson.annotations.SerializedName - -data class DeviceResponse( - @SerializedName("created") - var created: String = "", - @SerializedName("type") - var type: String = "", - @SerializedName("locale") - var locale: String = "", - @SerializedName("enabled") - var enabled: Boolean = false, - @SerializedName("token") - var token: String = "", - @SerializedName("waitlist_enabled") - var waitlistEnabled: Boolean = false, - @SerializedName("install_id") - var installId: String = "", - @SerializedName("warp_enabled") - var warpEnabled: Boolean = false, - @SerializedName("name") - var name: String = "", - @SerializedName("fcm_token") - var fcmToken: String = "", - @SerializedName("tos") - var tos: String = "", - @SerializedName("model") - var model: String = "", - @SerializedName("id") - var id: String = "", - @SerializedName("place") - var place: Int = 0, - @SerializedName("config") - var config: Config = Config(), - @SerializedName("updated") - var updated: String = "", - @SerializedName("key") - var key: String = "", - @SerializedName("account") - var account: Account = Account() -) { - data class Config( - @SerializedName("peers") - var peers: List = listOf(), - @SerializedName("services") - var services: Services = Services(), - @SerializedName("interface") - var interfaceX: Interface = Interface(), - @SerializedName("client_id") - var clientId: String = "" - ) { - data class Peer( - @SerializedName("public_key") - var publicKey: String = "", - @SerializedName("endpoint") - var endpoint: Endpoint = Endpoint() - ) { - data class Endpoint( - @SerializedName("v6") - var v6: String = "", - @SerializedName("host") - var host: String = "", - @SerializedName("v4") - var v4: String = "" - ) - } - - data class Services( - @SerializedName("http_proxy") - var httpProxy: String = "" - ) - - data class Interface( - @SerializedName("addresses") - var addresses: Addresses = Addresses() - ) { - data class Addresses( - @SerializedName("v6") - var v6: String = "", - @SerializedName("v4") - var v4: String = "" - ) - } - } - - data class Account( - @SerializedName("account_type") - var accountType: String = "", - @SerializedName("role") - var role: String = "", - @SerializedName("referral_renewal_countdown") - var referralRenewalCountdown: Int = 0, - @SerializedName("created") - var created: String = "", - @SerializedName("usage") - var usage: Int = 0, - @SerializedName("warp_plus") - var warpPlus: Boolean = false, - @SerializedName("referral_count") - var referralCount: Int = 0, - @SerializedName("license") - var license: String = "", - @SerializedName("quota") - var quota: Int = 0, - @SerializedName("premium_data") - var premiumData: Int = 0, - @SerializedName("id") - var id: String = "", - @SerializedName("updated") - var updated: String = "" - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt deleted file mode 100644 index 34bbe7c16..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.nekohasekai.sagernet.utils.cf - -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import com.wireguard.crypto.Key -import java.text.SimpleDateFormat -import java.util.* - -data class RegisterRequest( - @SerializedName("fcm_token") var fcmToken: String = "", - @SerializedName("install_id") var installedId: String = "", - var key: String = "", - var locale: String = "", - var model: String = "", - var tos: String = "", - var type: String = "" -) { - - companion object { - fun newRequest(publicKey: Key): String { - val request = RegisterRequest() - request.fcmToken = "" - request.installedId = "" - request.key = publicKey.toBase64() - request.locale = "en_US" - request.model = "PC" - val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'000000'+08:00", Locale.US) - request.tos = format.format(Date()) - request.type = "Android" - return Gson().toJson(request) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt deleted file mode 100644 index e12915b41..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.nekohasekai.sagernet.utils.cf - -import com.google.gson.Gson - -data class UpdateDeviceRequest( - var name: String, var active: Boolean -) { - companion object { - fun newRequest(name: String = "SagerNet Client", active: Boolean = true) = - Gson().toJson(UpdateDeviceRequest(name, active)) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt index b4f03afcc..d9da726a3 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt @@ -48,6 +48,9 @@ constructor( } else { linkLayout.isErrorEnabled = false } + if (link.contains("\n")) { + linkLayout.error = "Unexpected new line" + } } catch (e: Exception) { linkLayout.error = e.readableMessage linkLayout.isErrorEnabled = true diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/WindowInsetsListeners.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/WindowInsetsListeners.kt index fc35dd0f6..0f13ebc82 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/WindowInsetsListeners.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/WindowInsetsListeners.kt @@ -1,37 +1,9 @@ package io.nekohasekai.sagernet.widget import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.Insets -import androidx.core.view.* -import io.nekohasekai.sagernet.R - -object ListHolderListener : OnApplyWindowInsetsListener { - override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat { - val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()) - view.setPadding(statusBarInsets.left, - statusBarInsets.top, - statusBarInsets.right, - statusBarInsets.bottom) - return WindowInsetsCompat.Builder(insets).apply { - setInsets(WindowInsetsCompat.Type.statusBars(), Insets.NONE) - /*setInsets(WindowInsetsCompat.Type.navigationBars(), - insets.getInsets(WindowInsetsCompat.Type.navigationBars()))*/ - }.build() - } - - fun setup(activity: AppCompatActivity) = activity.findViewById(android.R.id.content).let { - ViewCompat.setOnApplyWindowInsetsListener(it, ListHolderListener) - WindowCompat.setDecorFitsSystemWindows(activity.window, false) - } -} - -object MainListListener : OnApplyWindowInsetsListener { - override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat) = insets.apply { - view.updatePadding(bottom = view.resources.getDimensionPixelOffset(R.dimen.main_list_padding_bottom) + - insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom) - } -} +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding object ListListener : OnApplyWindowInsetsListener { override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat) = insets.apply { diff --git a/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt b/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt new file mode 100644 index 000000000..f06e26d84 --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt @@ -0,0 +1,109 @@ +package moe.matsuri.nb4a + +import android.content.Context +import android.net.ConnectivityManager +import android.net.wifi.WifiManager +import android.os.Build +import android.os.Build.VERSION_CODES +import androidx.annotation.RequiresApi +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.bg.ServiceNotification +import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.database.SagerDatabase +import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.app +import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher +import io.nekohasekai.sagernet.utils.PackageCache +import libcore.BoxPlatformInterface +import libcore.Libcore +import libcore.NB4AInterface +import java.net.InetSocketAddress + +class NativeInterface : BoxPlatformInterface, NB4AInterface { + + // libbox interface + + override fun autoDetectInterfaceControl(fd: Int) { + DataStore.vpnService?.protect(fd) + } + + override fun openTun(singTunOptionsJson: String, tunPlatformOptionsJson: String): Long { + if (DataStore.vpnService == null) { + throw Exception("no VpnService") + } + return DataStore.vpnService!!.startVpn(singTunOptionsJson, tunPlatformOptionsJson).toLong() + } + + override fun useProcFS(): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun findConnectionOwner( + ipProto: Int, srcIp: String, srcPort: Int, destIp: String, destPort: Int + ): Int { + return SagerNet.connectivity.getConnectionOwnerUid( + ipProto, InetSocketAddress(srcIp, srcPort), InetSocketAddress(destIp, destPort) + ) + } + + override fun packageNameByUid(uid: Int): String { + PackageCache.awaitLoadSync() + + if (uid <= 1000L) { + return "android" + } + + val packageNames = PackageCache.uidMap[uid] + if (!packageNames.isNullOrEmpty()) for (packageName in packageNames) { + return packageName + } + + error("unknown uid $uid") + } + + override fun uidByPackageName(packageName: String): Int { + PackageCache.awaitLoadSync() + return PackageCache[packageName] ?: 0 + } + + // TODO: 'getter for connectionInfo: WifiInfo!' is deprecated + override fun wifiState(): String { + val wifiManager = + app.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val connectionInfo = wifiManager.connectionInfo + return "${connectionInfo.ssid},${connectionInfo.bssid}" + } + + // nb4a interface + + override fun useOfficialAssets(): Boolean { + return DataStore.rulesProvider == 0 + } + + override fun selector_OnProxySelected(selectorTag: String, tag: String) { + if (selectorTag != "proxy") { + Logs.d("other selector: $selectorTag") + return + } + Libcore.resetAllConnections(true) + DataStore.baseService?.apply { + runOnDefaultDispatcher { + val id = data.proxy!!.config.profileTagMap + .filterValues { it == tag }.keys.firstOrNull() ?: -1 + val ent = SagerDatabase.proxyDao.getById(id) ?: return@runOnDefaultDispatcher + // traffic & title + data.proxy?.apply { + looper?.selectMain(id) + displayProfileName = ServiceNotification.genTitle(ent) + data.notification?.postNotificationTitle(displayProfileName) + } + // post binder + data.binder.broadcast { b -> + b.cbSelectorUpdate(id) + } + } + } + } + +} diff --git a/app/src/main/java/moe/matsuri/nb4a/Protocols.kt b/app/src/main/java/moe/matsuri/nb4a/Protocols.kt index 1cda9044d..e280d6fcd 100644 --- a/app/src/main/java/moe/matsuri/nb4a/Protocols.kt +++ b/app/src/main/java/moe/matsuri/nb4a/Protocols.kt @@ -2,34 +2,14 @@ package moe.matsuri.nb4a import android.content.Context import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.ProxyEntity.Companion.TYPE_NEKO import io.nekohasekai.sagernet.fmt.AbstractBean import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ktx.getColorAttr -import moe.matsuri.nb4a.plugin.NekoPluginManager +import moe.matsuri.nb4a.proxy.config.ConfigBean // Settings for all protocols, built-in or plugin object Protocols { - // Mux - - fun shouldEnableMux(protocol: String): Boolean { - return DataStore.muxProtocols.contains(protocol) - } - - fun getCanMuxList(): List { - // built-in and support mux - // sing-box support ss & vmess & trojan smux - val list = mutableListOf("vmess", "trojan", "trojan-go", "shadowsocks") - - NekoPluginManager.getProtocols().forEach { - if (it.protocolConfig.optBoolean("canMux")) { - list.add(it.protocolId) - } - } - - return list - } // Deduplication @@ -38,6 +18,9 @@ object Protocols { ) { fun hash(): String { + if (bean is ConfigBean) { + return bean.config + } return bean.serverAddress + bean.serverPort + type } @@ -73,9 +56,11 @@ object Protocols { msgL.contains("timeout") || msgL.contains("deadline") -> { app.getString(R.string.connection_test_timeout) } + msgL.contains("refused") || msgL.contains("closed pipe") -> { app.getString(R.string.connection_test_refused) } + else -> msg } } diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java index 21fe7ba0b..05f89275f 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java @@ -1,19 +1,97 @@ package moe.matsuri.nb4a; -import static moe.matsuri.nb4a.utils.JavaUtil.gson; - +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.ToNumberPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; import java.util.List; import java.util.Map; +import moe.matsuri.nb4a.utils.Util; + public class SingBoxOptions { // base + private static final Gson gsonSingbox = new GsonBuilder() + .registerTypeHierarchyAdapter(SingBoxOption.class, new SingBoxOptionSerializer()) + .setPrettyPrinting() + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setLenient() + .disableHtmlEscaping() + .create(); + public static class SingBoxOption { + + public transient Map _hack_config_map; // 仍然用普通json方式合并,所以Object内不要使用 _hack + + public transient String _hack_custom_config; + + public SingBoxOption() { + _hack_config_map = new HashMap<>(); + } + public Map asMap() { - return gson.fromJson(gson.toJson(this), Map.class); + return gsonSingbox.fromJson(gsonSingbox.toJson(this), Map.class); + } + + } + + public static final class CustomSingBoxOption extends SingBoxOption { + + public transient String config; + + public CustomSingBoxOption(String config) { + super(); + this.config = config; + } + + public Map getBasicMap() { + Map map = gsonSingbox.fromJson(config, Map.class); + if (map == null) { + map = new HashMap<>(); + } + return map; + } + } + + // 自定义序列化器 + public static class SingBoxOptionSerializer implements JsonSerializer { + @Override + public JsonElement serialize(SingBoxOption src, Type typeOfSrc, JsonSerializationContext context) { + // 拿到原始的 delegate(默认序列化器) + TypeAdapter delegate = gsonSingbox.getDelegateAdapter( + new TypeAdapterFactory() { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + return null; // 返回 null,表示只作为“跳过当前自定义”的 marker + } + }, + TypeToken.get(src.getClass()) + ); + Map map; + if (src instanceof CustomSingBoxOption) { + map = ((CustomSingBoxOption) src).getBasicMap(); + } else { + map = gsonSingbox.fromJson(((TypeAdapter) delegate).toJson(src), Map.class); + } + if (src._hack_config_map != null && !src._hack_config_map.isEmpty()) { + Util.INSTANCE.mergeMap(map, src._hack_config_map); + } + if (src._hack_custom_config != null && !src._hack_custom_config.isBlank()) { + Util.INSTANCE.mergeJSON(map, src._hack_custom_config); + } + return gsonSingbox.toJsonTree(map); } } @@ -33,11 +111,12 @@ public static class MyOptions extends SingBoxOption { public List inbounds; - public List> outbounds; + public List outbounds; public RouteOptions route; public ExperimentalOptions experimental; + } // paste generate output here @@ -48,13 +127,15 @@ public static class ClashAPIOptions extends SingBoxOption { public String external_ui; + public String external_ui_download_url; + + public String external_ui_download_detour; + public String secret; public String default_mode; - public Boolean store_selected; - - public String cache_file; + // Generate note: option type: public List ModeList; } @@ -116,6 +197,8 @@ public static class LogOptions extends SingBoxOption { public static class DebugOptions extends SingBoxOption { + public String listen; + public Integer gc_percent; public Integer max_stack; @@ -142,6 +225,8 @@ public static class DirectInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -196,6 +281,8 @@ public static class DirectOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -223,6 +310,10 @@ public static class DNSOptions extends SingBoxOption { @SerializedName("final") public String final_; + public Boolean reverse_mapping; + + public DNSFakeIPOptions fakeip; + // Generate note: nested type DNSClientOptions public String strategy; @@ -230,17 +321,9 @@ public static class DNSOptions extends SingBoxOption { public Boolean disable_expire; - // End of public DNSClientOptions ; - - } - - public static class DNSClientOptions extends SingBoxOption { - - public String strategy; - - public Boolean disable_cache; + public Boolean independent_cache; - public Boolean disable_expire; + // End of public DNSClientOptions ; } @@ -262,117 +345,49 @@ public static class DNSServerOptions extends SingBoxOption { } + public static class DNSClientOptions extends SingBoxOption { - public static class DNSRule extends SingBoxOption { + public String strategy; - public String type; + public Boolean disable_cache; - // Generate note: option type: public DefaultDNSRule DefaultOptions; + public Boolean disable_expire; - // Generate note: option type: public LogicalDNSRule LogicalOptions; + public Boolean independent_cache; } - public static class DefaultDNSRule extends SingBoxOption { + public static class DNSFakeIPOptions extends SingBoxOption { - // Generate note: Listable - public List inbound; - - public Integer ip_version; - - // Generate note: Listable - public List query_type; - - public String network; - - // Generate note: Listable - public List auth_user; - - // Generate note: Listable - public List protocol; - - // Generate note: Listable - public List domain; - - // Generate note: Listable - public List domain_suffix; - - // Generate note: Listable - public List domain_keyword; - - // Generate note: Listable - public List domain_regex; - - // Generate note: Listable - public List geosite; - - // Generate note: Listable - public List source_geoip; - - // Generate note: Listable - public List source_ip_cidr; - - // Generate note: Listable - public List source_port; - - // Generate note: Listable - public List source_port_range; - - // Generate note: Listable - public List port; - - // Generate note: Listable - public List port_range; - - // Generate note: Listable - public List process_name; - - // Generate note: Listable - public List process_path; - - // Generate note: Listable - public List package_name; - - // Generate note: Listable - public List user; - - // Generate note: Listable - public List user_id; - - // Generate note: Listable - public List outbound; - - public String clash_mode; - - public Boolean invert; + public Boolean enabled; - public String server; + public String inet4_range; - public Boolean disable_cache; + public String inet6_range; } - public static class LogicalDNSRule extends SingBoxOption { - - public String mode; + public static class ExperimentalOptions extends SingBoxOption { - public List rules; + public ClashAPIOptions clash_api; - public Boolean invert; + public V2RayAPIOptions v2ray_api; - public String server; + public CacheFile cache_file; - public Boolean disable_cache; + public DebugOptions debug; } - public static class ExperimentalOptions extends SingBoxOption { + public static class CacheFile extends SingBoxOption { - public ClashAPIOptions clash_api; + public Boolean enabled; - public V2RayAPIOptions v2ray_api; + public Boolean store_fakeip; - public DebugOptions debug; + public String path; + + public String cache_id; } @@ -385,6 +400,8 @@ public static class HysteriaInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -466,6 +483,8 @@ public static class HysteriaOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -508,6 +527,136 @@ public static class HysteriaOutboundOptions extends SingBoxOption { public OutboundTLSOptions tls; + public List server_ports; + + public String hop_interval; + + } + + public static class Hysteria2InboundOptions extends SingBoxOption { + + // Generate note: nested type ListenOptions + public String listen; + + public Integer listen_port; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + // Generate note: option type: public Boolean UDPFragmentDefault; + + public Long udp_timeout; + + public Boolean proxy_protocol; + + public Boolean proxy_protocol_accept_no_header; + + public String detour; + + // Generate note: nested type InboundOptions + public Boolean sniff; + + public Boolean sniff_override_destination; + + public Long sniff_timeout; + + public String domain_strategy; + + // End of public InboundOptions ; + + // End of public ListenOptions ; + + public Integer up_mbps; + + public Integer down_mbps; + + public Hysteria2Obfs obfs; + + public List users; + + public Boolean ignore_client_bandwidth; + + public InboundTLSOptions tls; + + public String masquerade; + + } + + public static class Hysteria2Obfs extends SingBoxOption { + + public String type; + + public String password; + + } + + public static class Hysteria2User extends SingBoxOption { + + public String name; + + public String password; + + } + + public static class Hysteria2OutboundOptions extends SingBoxOption { + + // Generate note: nested type DialerOptions + public String detour; + + public String bind_interface; + + public String inet4_bind_address; + + public String inet6_bind_address; + + public String protect_path; + + public Integer routing_mark; + + public Boolean reuse_addr; + + public Long connect_timeout; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + // Generate note: option type: public Boolean UDPFragmentDefault; + + public String domain_strategy; + + public Long fallback_delay; + + // End of public DialerOptions ; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // End of public ServerOptions ; + + public Integer up_mbps; + + public Integer down_mbps; + + public Hysteria2Obfs obfs; + + public String password; + + public String network; + + public OutboundTLSOptions tls; + + public List server_ports; + + public String hop_interval; + } @@ -545,6 +694,10 @@ public static class Inbound extends SingBoxOption { // Generate note: option type: public VLESSInboundOptions VLESSOptions; + // Generate note: option type: public TUICInboundOptions TUICOptions; + + // Generate note: option type: public Hysteria2InboundOptions Hysteria2Options; + } public static class InboundOptions extends SingBoxOption { @@ -567,6 +720,8 @@ public static class ListenOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -601,6 +756,8 @@ public static class NaiveInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -668,6 +825,8 @@ public static class NTPOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -711,7 +870,11 @@ public static class Outbound extends SingBoxOption { // Generate note: option type: public ShadowsocksROutboundOptions ShadowsocksROptions; - // Generate note: option type: public VLESSOutboundOptions VLESSOptions; + // Generate note: option type: public VLESSOutboundOptions VLESSOptions; + + // Generate note: option type: public TUICOutboundOptions TUICOptions; + + // Generate note: option type: public Hysteria2OutboundOptions Hysteria2Options; // Generate note: option type: public SelectorOutboundOptions SelectorOptions; @@ -739,6 +902,8 @@ public static class DialerOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -769,6 +934,8 @@ public static class MultiplexOptions extends SingBoxOption { public Integer max_streams; + public Boolean padding; + } public static class OnDemandOptions extends SingBoxOption { @@ -808,6 +975,8 @@ public static class RedirectInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -844,6 +1013,8 @@ public static class TProxyInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -875,12 +1046,10 @@ public static class TProxyInboundOptions extends SingBoxOption { public static class RouteOptions extends SingBoxOption { - public GeoIPOptions geoip; - - public GeositeOptions geosite; - public List rules; + public List rule_set; + @SerializedName("final") public String final_; @@ -896,34 +1065,28 @@ public static class RouteOptions extends SingBoxOption { } - public static class GeoIPOptions extends SingBoxOption { - public String path; - - public String download_url; - - public String download_detour; - - } - - public static class GeositeOptions extends SingBoxOption { + public static class Rule extends SingBoxOption { - public String path; + public String type; - public String download_url; + // Generate note: option type: public DefaultRule DefaultOptions; - public String download_detour; + // Generate note: option type: public LogicalRule LogicalOptions; } - - public static class Rule extends SingBoxOption { + public static class RuleSet extends SingBoxOption { public String type; - // Generate note: option type: public DefaultRule DefaultOptions; + public String tag; - // Generate note: option type: public LogicalRule LogicalOptions; + public String format; + + public String path; + + public String url; } @@ -934,7 +1097,8 @@ public static class DefaultRule extends SingBoxOption { public Integer ip_version; - public String network; + // Generate note: Listable + public List network; // Generate note: Listable public List auth_user; @@ -954,15 +1118,6 @@ public static class DefaultRule extends SingBoxOption { // Generate note: Listable public List domain_regex; - // Generate note: Listable - public List geosite; - - // Generate note: Listable - public List source_geoip; - - // Generate note: Listable - public List geoip; - // Generate note: Listable public List source_ip_cidr; @@ -1004,15 +1159,93 @@ public static class DefaultRule extends SingBoxOption { } - public static class LogicalRule extends SingBoxOption { - public String mode; + public static class DNSRule extends SingBoxOption { + + public String type; + + // Generate note: option type: public DefaultDNSRule DefaultOptions; + + // Generate note: option type: public LogicalDNSRule LogicalOptions; + + } + + public static class DefaultDNSRule extends SingBoxOption { + + // Generate note: Listable + public List inbound; + + public Integer ip_version; + + // Generate note: Listable + public List query_type; + + // Generate note: Listable + public List network; + + // Generate note: Listable + public List auth_user; + + // Generate note: Listable + public List protocol; + + // Generate note: Listable + public List domain; + + // Generate note: Listable + public List domain_suffix; + + // Generate note: Listable + public List domain_keyword; + + // Generate note: Listable + public List domain_regex; + + // Generate note: Listable + public List geosite; + + // Generate note: Listable + public List source_ip_cidr; + + // Generate note: Listable + public List source_port; + + // Generate note: Listable + public List source_port_range; + + // Generate note: Listable + public List port; + + // Generate note: Listable + public List port_range; + + // Generate note: Listable + public List process_name; + + // Generate note: Listable + public List process_path; + + // Generate note: Listable + public List package_name; + + // Generate note: Listable + public List user; + + // Generate note: Listable + public List user_id; + + // Generate note: Listable + public List outbound; - public List rules; + public String clash_mode; public Boolean invert; - public String outbound; + public String server; + + public Boolean disable_cache; + + public Integer rewrite_ttl; } @@ -1025,6 +1258,8 @@ public static class ShadowsocksInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1106,6 +1341,8 @@ public static class ShadowsocksOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1160,6 +1397,8 @@ public static class ShadowsocksROutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1202,6 +1441,8 @@ public static class ShadowTLSInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1277,6 +1518,8 @@ public static class ShadowTLSHandshakeOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1310,6 +1553,8 @@ public static class ShadowTLSOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1344,6 +1589,8 @@ public static class SocksInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1382,6 +1629,8 @@ public static class HTTPMixedInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1436,6 +1685,8 @@ public static class SocksOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1486,6 +1737,8 @@ public static class HTTPOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1509,6 +1762,10 @@ public static class HTTPOutboundOptions extends SingBoxOption { public OutboundTLSOptions tls; + public String path; + + public Map headers; + } public static class SSHOutboundOptions extends SingBoxOption { @@ -1532,6 +1789,8 @@ public static class SSHOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1587,16 +1846,20 @@ public static class InboundTLSOptions extends SingBoxOption { // Generate note: Listable public List cipher_suites; - public String certificate; + // Generate note: Listable + public List certificate; public String certificate_path; - public String key; + // Generate note: Listable + public List key; public String key_path; public InboundACMEOptions acme; + public InboundECHOptions ech; + public InboundRealityOptions reality; } @@ -1676,6 +1939,8 @@ public static class InboundRealityHandshakeOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1688,15 +1953,25 @@ public static class InboundRealityHandshakeOptions extends SingBoxOption { } - public static class OutboundECHOptions extends SingBoxOption { + public static class InboundECHOptions extends SingBoxOption { public Boolean enabled; - public Boolean pq_signature_schemes_enabled; + // Generate note: Listable + public List key; + + public String key_path; + + } + + public static class OutboundECHOptions extends SingBoxOption { + + public Boolean enabled; - public Boolean dynamic_record_sizing_disabled; + // Generate note: Listable + public List config; - public String config; + public String config_path; } @@ -1772,6 +2047,8 @@ public static class TorOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1801,6 +2078,116 @@ public static class TrojanInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + // Generate note: option type: public Boolean UDPFragmentDefault; + + public Long udp_timeout; + + public Boolean proxy_protocol; + + public Boolean proxy_protocol_accept_no_header; + + public String detour; + + // Generate note: nested type InboundOptions + public Boolean sniff; + + public Boolean sniff_override_destination; + + public Long sniff_timeout; + + public String domain_strategy; + + // End of public InboundOptions ; + + // End of public ListenOptions ; + + public List users; + + public InboundTLSOptions tls; + + public ServerOptions fallback; + + public Map fallback_for_alpn; + + public V2RayTransportOptions transport; + + } + + public static class TrojanUser extends SingBoxOption { + + public String name; + + public String password; + + } + + public static class TrojanOutboundOptions extends SingBoxOption { + + // Generate note: nested type DialerOptions + public String detour; + + public String bind_interface; + + public String inet4_bind_address; + + public String inet6_bind_address; + + public String protect_path; + + public Integer routing_mark; + + public Boolean reuse_addr; + + public Long connect_timeout; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + // Generate note: option type: public Boolean UDPFragmentDefault; + + public String domain_strategy; + + public Long fallback_delay; + + // End of public DialerOptions ; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // End of public ServerOptions ; + + public String password; + + public String network; + + public OutboundTLSOptions tls; + + public MultiplexOptions multiplex; + + public V2RayTransportOptions transport; + + } + + public static class TUICInboundOptions extends SingBoxOption { + + // Generate note: nested type ListenOptions + public String listen; + + public Integer listen_port; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1826,27 +2213,31 @@ public static class TrojanInboundOptions extends SingBoxOption { // End of public ListenOptions ; - public List users; + public List users; - public InboundTLSOptions tls; + public String congestion_control; - public ServerOptions fallback; + public Long auth_timeout; - public Map fallback_for_alpn; + public Boolean zero_rtt_handshake; - public V2RayTransportOptions transport; + public Long heartbeat; + + public InboundTLSOptions tls; } - public static class TrojanUser extends SingBoxOption { + public static class TUICUser extends SingBoxOption { public String name; + public String uuid; + public String password; } - public static class TrojanOutboundOptions extends SingBoxOption { + public static class TUICOutboundOptions extends SingBoxOption { // Generate note: nested type DialerOptions public String detour; @@ -1867,6 +2258,8 @@ public static class TrojanOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -1884,15 +2277,23 @@ public static class TrojanOutboundOptions extends SingBoxOption { // End of public ServerOptions ; + public String uuid; + public String password; - public String network; + public String congestion_control; - public OutboundTLSOptions tls; + public String udp_relay_mode; - public MultiplexOptions multiplex; + public Boolean udp_over_stream; - public V2RayTransportOptions transport; + public Boolean zero_rtt_handshake; + + public Long heartbeat; + + public String network; + + public OutboundTLSOptions tls; } @@ -1918,6 +2319,12 @@ public static class TunInboundOptions extends SingBoxOption { // Generate note: Listable public List inet6_route_address; + // Generate note: Listable + public List include_interface; + + // Generate note: Listable + public List exclude_interface; + // Generate note: Listable public List include_uid; @@ -2067,6 +2474,16 @@ public static class V2RayGRPCOptions extends SingBoxOption { } + public static class V2RayHTTPUpgradeOptions extends SingBoxOption { + + public String host; + + public String path; + + public Map headers; + + } + public static class VLESSInboundOptions extends SingBoxOption { // Generate note: nested type ListenOptions @@ -2076,6 +2493,8 @@ public static class VLESSInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -2140,6 +2559,8 @@ public static class VLESSOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -2165,6 +2586,8 @@ public static class VLESSOutboundOptions extends SingBoxOption { public OutboundTLSOptions tls; + public MultiplexOptions multiplex; + public V2RayTransportOptions transport; public String packet_encoding; @@ -2180,6 +2603,8 @@ public static class VMessInboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -2244,6 +2669,8 @@ public static class VMessOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -2304,6 +2731,8 @@ public static class WireGuardOutboundOptions extends SingBoxOption { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; // Generate note: option type: public Boolean UDPFragmentDefault; @@ -2314,13 +2743,6 @@ public static class WireGuardOutboundOptions extends SingBoxOption { // End of public DialerOptions ; - // Generate note: nested type ServerOptions - public String server; - - public Integer server_port; - - // End of public ServerOptions ; - public Boolean system_interface; public String interface_name; @@ -2330,6 +2752,15 @@ public static class WireGuardOutboundOptions extends SingBoxOption { public String private_key; + public List peers; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // End of public ServerOptions ; + public String peer_public_key; public String pre_shared_key; @@ -2345,96 +2776,24 @@ public static class WireGuardOutboundOptions extends SingBoxOption { } - public static class DNSRule_DefaultOptions extends DNSRule { - - // Generate note: Listable - public List inbound; - - public Integer ip_version; - - // Generate note: Listable - public List query_type; - - public String network; - - // Generate note: Listable - public List auth_user; - - // Generate note: Listable - public List protocol; - - // Generate note: Listable - public List domain; - - // Generate note: Listable - public List domain_suffix; - - // Generate note: Listable - public List domain_keyword; - - // Generate note: Listable - public List domain_regex; - - // Generate note: Listable - public List geosite; - - // Generate note: Listable - public List source_geoip; - - // Generate note: Listable - public List source_ip_cidr; - - // Generate note: Listable - public List source_port; - - // Generate note: Listable - public List source_port_range; - - // Generate note: Listable - public List port; - - // Generate note: Listable - public List port_range; - - // Generate note: Listable - public List process_name; - - // Generate note: Listable - public List process_path; - - // Generate note: Listable - public List package_name; - - // Generate note: Listable - public List user; - - // Generate note: Listable - public List user_id; - - // Generate note: Listable - public List outbound; - - public String clash_mode; - - public Boolean invert; + public static class WireGuardPeer extends SingBoxOption { + // Generate note: nested type ServerOptions public String server; - public Boolean disable_cache; - - } - - public static class DNSRule_LogicalOptions extends DNSRule { + public Integer server_port; - public String mode; + // End of public ServerOptions ; - public List rules; + public String public_key; - public Boolean invert; + public String pre_shared_key; - public String server; + // Generate note: Listable + public List allowed_ips; - public Boolean disable_cache; + // Generate note: Base64 String + public String reserved; } @@ -2460,6 +2819,12 @@ public static class Inbound_TunOptions extends Inbound { // Generate note: Listable public List inet6_route_address; + // Generate note: Listable + public List include_interface; + + // Generate note: Listable + public List exclude_interface; + // Generate note: Listable public List include_uid; @@ -2511,6 +2876,8 @@ public static class Inbound_RedirectOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2546,6 +2913,8 @@ public static class Inbound_TProxyOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2583,6 +2952,8 @@ public static class Inbound_DirectOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2624,6 +2995,8 @@ public static class Inbound_SocksOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2661,6 +3034,8 @@ public static class Inbound_HTTPOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2702,6 +3077,8 @@ public static class Inbound_MixedOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2743,6 +3120,8 @@ public static class Inbound_ShadowsocksOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2788,6 +3167,8 @@ public static class Inbound_VMessOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2829,6 +3210,8 @@ public static class Inbound_TrojanOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2874,6 +3257,8 @@ public static class Inbound_NaiveOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2915,6 +3300,8 @@ public static class Inbound_HysteriaOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -2972,6 +3359,8 @@ public static class Inbound_ShadowTLSOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3019,6 +3408,8 @@ public static class Inbound_VLESSOptions extends Inbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3051,6 +3442,106 @@ public static class Inbound_VLESSOptions extends Inbound { } + public static class Inbound_TUICOptions extends Inbound { + + // Generate note: nested type ListenOptions + public String listen; + + public Integer listen_port; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + + public Long udp_timeout; + + public Boolean proxy_protocol; + + public Boolean proxy_protocol_accept_no_header; + + public String detour; + + // Generate note: nested type InboundOptions + public Boolean sniff; + + public Boolean sniff_override_destination; + + public Long sniff_timeout; + + public String domain_strategy; + + // End of public InboundOptions ; + + // End of public ListenOptions ; + + public List users; + + public String congestion_control; + + public Long auth_timeout; + + public Boolean zero_rtt_handshake; + + public Long heartbeat; + + public InboundTLSOptions tls; + + } + + public static class Inbound_Hysteria2Options extends Inbound { + + // Generate note: nested type ListenOptions + public String listen; + + public Integer listen_port; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + + public Long udp_timeout; + + public Boolean proxy_protocol; + + public Boolean proxy_protocol_accept_no_header; + + public String detour; + + // Generate note: nested type InboundOptions + public Boolean sniff; + + public Boolean sniff_override_destination; + + public Long sniff_timeout; + + public String domain_strategy; + + // End of public InboundOptions ; + + // End of public ListenOptions ; + + public Integer up_mbps; + + public Integer down_mbps; + + public Hysteria2Obfs obfs; + + public List users; + + public Boolean ignore_client_bandwidth; + + public InboundTLSOptions tls; + + public String masquerade; + + } + public static class Outbound_DirectOptions extends Outbound { // Generate note: nested type DialerOptions @@ -3072,6 +3563,8 @@ public static class Outbound_DirectOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3110,6 +3603,8 @@ public static class Outbound_SocksOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3159,6 +3654,8 @@ public static class Outbound_HTTPOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3181,6 +3678,10 @@ public static class Outbound_HTTPOptions extends Outbound { public OutboundTLSOptions tls; + public String path; + + public Map headers; + } public static class Outbound_ShadowsocksOptions extends Outbound { @@ -3204,6 +3705,8 @@ public static class Outbound_ShadowsocksOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3257,6 +3760,8 @@ public static class Outbound_VMessOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3316,6 +3821,8 @@ public static class Outbound_TrojanOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3365,6 +3872,8 @@ public static class Outbound_WireGuardOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3374,13 +3883,6 @@ public static class Outbound_WireGuardOptions extends Outbound { // End of public DialerOptions ; - // Generate note: nested type ServerOptions - public String server; - - public Integer server_port; - - // End of public ServerOptions ; - public Boolean system_interface; public String interface_name; @@ -3390,6 +3892,15 @@ public static class Outbound_WireGuardOptions extends Outbound { public String private_key; + public List peers; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // End of public ServerOptions ; + public String peer_public_key; public String pre_shared_key; @@ -3426,6 +3937,8 @@ public static class Outbound_HysteriaOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3467,6 +3980,10 @@ public static class Outbound_HysteriaOptions extends Outbound { public OutboundTLSOptions tls; + public List server_ports; + + public String hop_interval; + } public static class Outbound_TorOptions extends Outbound { @@ -3490,6 +4007,8 @@ public static class Outbound_TorOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3530,6 +4049,8 @@ public static class Outbound_SSHOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3587,6 +4108,8 @@ public static class Outbound_ShadowTLSOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3632,6 +4155,8 @@ public static class Outbound_ShadowsocksROptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3685,6 +4210,8 @@ public static class Outbound_VLESSOptions extends Outbound { public Boolean tcp_fast_open; + public Boolean tcp_multi_path; + public Boolean udp_fragment; @@ -3709,12 +4236,130 @@ public static class Outbound_VLESSOptions extends Outbound { public OutboundTLSOptions tls; + public MultiplexOptions multiplex; + public V2RayTransportOptions transport; public String packet_encoding; } + public static class Outbound_TUICOptions extends Outbound { + + // Generate note: nested type DialerOptions + public String detour; + + public String bind_interface; + + public String inet4_bind_address; + + public String inet6_bind_address; + + public String protect_path; + + public Integer routing_mark; + + public Boolean reuse_addr; + + public Long connect_timeout; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + + public String domain_strategy; + + public Long fallback_delay; + + // End of public DialerOptions ; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // End of public ServerOptions ; + + public String uuid; + + public String password; + + public String congestion_control; + + public String udp_relay_mode; + + public Boolean udp_over_stream; + + public Boolean zero_rtt_handshake; + + public Long heartbeat; + + public String network; + + public OutboundTLSOptions tls; + + } + + public static class Outbound_Hysteria2Options extends Outbound { + + // Generate note: nested type DialerOptions + public String detour; + + public String bind_interface; + + public String inet4_bind_address; + + public String inet6_bind_address; + + public String protect_path; + + public Integer routing_mark; + + public Boolean reuse_addr; + + public Long connect_timeout; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + + public String domain_strategy; + + public Long fallback_delay; + + // End of public DialerOptions ; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // End of public ServerOptions ; + + public Integer up_mbps; + + public Integer down_mbps; + + public Hysteria2Obfs obfs; + + public String password; + + public String network; + + public OutboundTLSOptions tls; + + public List server_ports; + + public String hop_interval; + + } + public static class Outbound_SelectorOptions extends Outbound { public List outbounds; @@ -3743,7 +4388,8 @@ public static class Rule_DefaultOptions extends Rule { public Integer ip_version; - public String network; + // Generate note: Listable + public List network; // Generate note: Listable public List auth_user; @@ -3763,14 +4409,11 @@ public static class Rule_DefaultOptions extends Rule { // Generate note: Listable public List domain_regex; - // Generate note: Listable - public List geosite; + public List rule_set; - // Generate note: Listable - public List source_geoip; + public Boolean source_ip_is_private; - // Generate note: Listable - public List geoip; + public Boolean ip_is_private; // Generate note: Listable public List source_ip_cidr; @@ -3809,19 +4452,87 @@ public static class Rule_DefaultOptions extends Rule { public Boolean invert; + public String action; + public String outbound; } - public static class Rule_LogicalOptions extends Rule { + public static class DNSRule_DefaultOptions extends DNSRule { + + // Generate note: Listable + public List inbound; + + public Integer ip_version; + + // Generate note: Listable + public List query_type; + + // Generate note: Listable + public List network; + + // Generate note: Listable + public List auth_user; + + // Generate note: Listable + public List protocol; + + // Generate note: Listable + public List domain; + + // Generate note: Listable + public List domain_suffix; + + // Generate note: Listable + public List domain_keyword; + + // Generate note: Listable + public List domain_regex; + + public List rule_set; + + // Generate note: Listable + public List source_ip_cidr; + + // Generate note: Listable + public List source_port; + + // Generate note: Listable + public List source_port_range; + + // Generate note: Listable + public List port; + + // Generate note: Listable + public List port_range; + + // Generate note: Listable + public List process_name; + + // Generate note: Listable + public List process_path; + + // Generate note: Listable + public List package_name; + + // Generate note: Listable + public List user; + + // Generate note: Listable + public List user_id; - public String mode; + // Generate note: Listable + public List outbound; - public List rules; + public String clash_mode; public Boolean invert; - public String outbound; + public String server; + + public Boolean disable_cache; + + public Integer rewrite_ttl; } @@ -3868,4 +4579,66 @@ public static class V2RayTransportOptions_GRPCOptions extends V2RayTransportOpti } + public static class V2RayTransportOptions_HTTPUpgradeOptions extends V2RayTransportOptions { + + public String host; + + public String path; + + + } + + // sing-box Options 生成器已经坏了,以下是从 husi 抄的 + + public static class Outbound_AnyTLSOptions extends Outbound { + + // Generate note: nested type DialerOptions + public String detour; + + public String bind_interface; + + public String inet4_bind_address; + + public String inet6_bind_address; + + public String protect_path; + + public Integer routing_mark; + + public Boolean reuse_addr; + + public String connect_timeout; + + public Boolean tcp_fast_open; + + public Boolean tcp_multi_path; + + public Boolean udp_fragment; + + public String domain_strategy; + + public String network_strategy; + + public List network_type; + + public List fallback_network_type; + + public String fallback_delay; + + // Generate note: nested type ServerOptions + public String server; + + public Integer server_port; + + // Generate note: nested type OutboundTLSOptionsContainer + public OutboundTLSOptions tls; + + public String password; + + public String idle_session_check_interval; + + public String idle_session_timeout; + + } + } diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt index e4186d9ed..d3971b2b2 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt @@ -1,26 +1,41 @@ package moe.matsuri.nb4a import io.nekohasekai.sagernet.database.DataStore +import moe.matsuri.nb4a.SingBoxOptions.RuleSet -fun SingBoxOptions.DNSServerOptions.applyDNSNetworkSettings(isDirect: Boolean) { - if (isDirect) { - if (DataStore.dnsNetwork.contains("NoDirectIPv4")) this.strategy = "ipv6_only" - if (DataStore.dnsNetwork.contains("NoDirectIPv6")) this.strategy = "ipv4_only" - } else { - if (DataStore.dnsNetwork.contains("NoRemoteIPv4")) this.strategy = "ipv6_only" - if (DataStore.dnsNetwork.contains("NoRemoteIPv6")) this.strategy = "ipv4_only" +object SingBoxOptionsUtil { + + fun domainStrategy(tag: String): String { + fun auto2(key: String, newS: String): String { + return (DataStore.configurationStore.getString(key) ?: "").replace("auto", newS) + } + return when (tag) { + "dns-remote" -> { + auto2("domain_strategy_for_remote", "") + } + + "dns-direct" -> { + auto2("domain_strategy_for_direct", "") + } + + // server + else -> { + auto2("domain_strategy_for_server", "prefer_ipv4") + } + } } + } fun SingBoxOptions.DNSRule_DefaultOptions.makeSingBoxRule(list: List) { - geosite = mutableListOf() + rule_set = mutableListOf() domain = mutableListOf() domain_suffix = mutableListOf() domain_regex = mutableListOf() domain_keyword = mutableListOf() list.forEach { if (it.startsWith("geosite:")) { - geosite.plusAssign(it.removePrefix("geosite:")) + rule_set.plusAssign(it) } else if (it.startsWith("full:")) { domain.plusAssign(it.removePrefix("full:").lowercase()) } else if (it.startsWith("domain:")) { @@ -30,16 +45,15 @@ fun SingBoxOptions.DNSRule_DefaultOptions.makeSingBoxRule(list: List) { } else if (it.startsWith("keyword:")) { domain_keyword.plusAssign(it.removePrefix("keyword:").lowercase()) } else { - // https://github.com/SagerNet/sing-box/commit/5d41e328d4a9f7549dd27f11b4ccc43710a73664 - domain.plusAssign(it.lowercase()) + domain_suffix.plusAssign(it.lowercase()) } } - geosite?.removeIf { it.isNullOrBlank() } + rule_set?.removeIf { it.isNullOrBlank() } domain?.removeIf { it.isNullOrBlank() } domain_suffix?.removeIf { it.isNullOrBlank() } domain_regex?.removeIf { it.isNullOrBlank() } domain_keyword?.removeIf { it.isNullOrBlank() } - if (geosite?.isEmpty() == true) geosite = null + if (rule_set?.isEmpty() == true) rule_set = null if (domain?.isEmpty() == true) domain = null if (domain_suffix?.isEmpty() == true) domain_suffix = null if (domain_regex?.isEmpty() == true) domain_regex = null @@ -47,7 +61,7 @@ fun SingBoxOptions.DNSRule_DefaultOptions.makeSingBoxRule(list: List) { } fun SingBoxOptions.DNSRule_DefaultOptions.checkEmpty(): Boolean { - if (geosite?.isNotEmpty() == true) return false + if (rule_set?.isNotEmpty() == true) return false if (domain?.isNotEmpty() == true) return false if (domain_suffix?.isNotEmpty() == true) return false if (domain_regex?.isNotEmpty() == true) return false @@ -56,12 +70,36 @@ fun SingBoxOptions.DNSRule_DefaultOptions.checkEmpty(): Boolean { return true } +fun generateRuleSet(ruleSetString: List, ruleSet: MutableList) { + ruleSetString.forEach { + when { + it.startsWith("geoip:") -> { + ruleSet.add(RuleSet().apply { + type = "local" + tag = it + format = "binary" + path = it + }) + } + + it.startsWith("geosite:") -> { + ruleSet.add(RuleSet().apply { + type = "local" + tag = it + format = "binary" + path = it + }) + } + } + } +} + fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: Boolean) { if (isIP) { ip_cidr = mutableListOf() - geoip = mutableListOf() + rule_set = mutableListOf() } else { - geosite = mutableListOf() + rule_set = mutableListOf() domain = mutableListOf() domain_suffix = mutableListOf() domain_regex = mutableListOf() @@ -70,14 +108,18 @@ fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: list.forEach { if (isIP) { if (it.startsWith("geoip:")) { - geoip.plusAssign(it.removePrefix("geoip:")) + if (it == "geoip:private") { + ip_is_private = true + } else { + rule_set.plusAssign(it) + } } else { ip_cidr.plusAssign(it) } return@forEach } if (it.startsWith("geosite:")) { - geosite.plusAssign(it.removePrefix("geosite:")) + rule_set.plusAssign(it) } else if (it.startsWith("full:")) { domain.plusAssign(it.removePrefix("full:").lowercase()) } else if (it.startsWith("domain:")) { @@ -87,20 +129,16 @@ fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: } else if (it.startsWith("keyword:")) { domain_keyword.plusAssign(it.removePrefix("keyword:").lowercase()) } else { - // https://github.com/SagerNet/sing-box/commit/5d41e328d4a9f7549dd27f11b4ccc43710a73664 - domain.plusAssign(it.lowercase()) + domain_suffix.plusAssign(it.lowercase()) } } ip_cidr?.removeIf { it.isNullOrBlank() } - geoip?.removeIf { it.isNullOrBlank() } - geosite?.removeIf { it.isNullOrBlank() } + rule_set?.removeIf { it.isNullOrBlank() } domain?.removeIf { it.isNullOrBlank() } domain_suffix?.removeIf { it.isNullOrBlank() } domain_regex?.removeIf { it.isNullOrBlank() } domain_keyword?.removeIf { it.isNullOrBlank() } if (ip_cidr?.isEmpty() == true) ip_cidr = null - if (geoip?.isEmpty() == true) geoip = null - if (geosite?.isEmpty() == true) geosite = null if (domain?.isEmpty() == true) domain = null if (domain_suffix?.isEmpty() == true) domain_suffix = null if (domain_regex?.isEmpty() == true) domain_regex = null @@ -109,9 +147,8 @@ fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: fun SingBoxOptions.Rule_DefaultOptions.checkEmpty(): Boolean { if (ip_cidr?.isNotEmpty() == true) return false - if (geoip?.isNotEmpty() == true) return false - if (geosite?.isNotEmpty() == true) return false if (domain?.isNotEmpty() == true) return false + if (rule_set?.isNotEmpty() == true) return false if (domain_suffix?.isNotEmpty() == true) return false if (domain_regex?.isNotEmpty() == true) return false if (domain_keyword?.isNotEmpty() == true) return false @@ -120,5 +157,7 @@ fun SingBoxOptions.Rule_DefaultOptions.checkEmpty(): Boolean { if (port?.isNotEmpty() == true) return false if (port_range?.isNotEmpty() == true) return false if (source_ip_cidr?.isNotEmpty() == true) return false + // + if (!_hack_custom_config.isNullOrBlank()) return false return true } diff --git a/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt b/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt index 8a5a21907..9b125aeea 100644 --- a/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt +++ b/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt @@ -3,77 +3,148 @@ package moe.matsuri.nb4a.net import android.net.DnsResolver import android.os.Build import android.os.CancellationSignal +import android.system.ErrnoException +import androidx.annotation.RequiresApi import io.nekohasekai.sagernet.SagerNet -import io.nekohasekai.sagernet.ktx.tryResume -import io.nekohasekai.sagernet.ktx.tryResumeWithException +import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.runOnIoDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.runBlocking +import libcore.ExchangeContext +import libcore.LocalDNSTransport import java.net.InetAddress -import kotlin.coroutines.suspendCoroutine +import java.net.UnknownHostException -object LocalResolverImpl : libcore.LocalResolver { +object LocalResolverImpl : LocalDNSTransport { - override fun lookupIP(network: String, domain: String): String { + // new local + + private const val RCODE_NXDOMAIN = 3 + + override fun raw(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + } + + override fun networkHandle(): Long { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return runBlocking { - suspendCoroutine { continuation -> - val signal = CancellationSignal() - val callback = object : DnsResolver.Callback> { - @Suppress("ThrowableNotThrown") - override fun onAnswer(answer: Collection, rcode: Int) { - // libcore/v2ray.go - when { - answer.isNotEmpty() -> { - continuation.tryResume((answer as Collection).mapNotNull { it?.hostAddress } - .joinToString(",")) - } - rcode == 0 -> { - // fuck AAAA no record - // features/dns/client.go - continuation.tryResume("") - } - else -> { - // Need return rcode - // proxy/dns/dns.go - continuation.tryResumeWithException(Exception("$rcode")) - } - } - } + return SagerNet.underlyingNetwork?.networkHandle ?: 0 + } + return 0 + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun exchange(ctx: ExchangeContext, message: ByteArray) { + val signal = CancellationSignal() + ctx.onCancel(signal::cancel) + + val callback = object : DnsResolver.Callback { + override fun onAnswer(answer: ByteArray, rcode: Int) { + ctx.rawSuccess(answer) + } + + override fun onError(error: DnsResolver.DnsException) { + val cause = error.cause + if (cause is ErrnoException) { + ctx.errnoCode(cause.errno) + } else { + Logs.w(error) + ctx.errnoCode(114514) + } + } + } - override fun onError(error: DnsResolver.DnsException) { - continuation.tryResumeWithException(error) + DnsResolver.getInstance().rawQuery( + SagerNet.underlyingNetwork, + message, + DnsResolver.FLAG_NO_RETRY, + Dispatchers.IO.asExecutor(), + signal, + callback + ) + } + + override fun lookup(ctx: ExchangeContext, network: String, domain: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val signal = CancellationSignal() + ctx.onCancel(signal::cancel) + + val callback = object : DnsResolver.Callback> { + override fun onAnswer(answer: Collection, rcode: Int) { + try { + if (rcode == 0) { + ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n")) + } else { + ctx.errorCode(rcode) } + } catch (e: Exception) { + Logs.w(e) + ctx.errnoCode(114514) } - val type = when { - network.endsWith("4") -> DnsResolver.TYPE_A - network.endsWith("6") -> DnsResolver.TYPE_AAAA - else -> null + } + + override fun onError(error: DnsResolver.DnsException) { + try { + val cause = error.cause + if (cause is ErrnoException) { + ctx.errnoCode(cause.errno) + } else { + Logs.w(error) + ctx.errnoCode(114514) + } + } catch (e: Exception) { + Logs.w(e) + ctx.errnoCode(114514) } - if (type != null) { - DnsResolver.getInstance().query( - SagerNet.underlyingNetwork, - domain, - type, - DnsResolver.FLAG_EMPTY, - Dispatchers.IO.asExecutor(), - signal, - callback - ) + } + } + + val type = when { + network.endsWith("4") -> DnsResolver.TYPE_A + network.endsWith("6") -> DnsResolver.TYPE_AAAA + else -> null + } + if (type != null) { + DnsResolver.getInstance().query( + SagerNet.underlyingNetwork, + domain, + type, + DnsResolver.FLAG_NO_RETRY, + Dispatchers.IO.asExecutor(), + signal, + callback + ) + } else { + DnsResolver.getInstance().query( + SagerNet.underlyingNetwork, + domain, + DnsResolver.FLAG_NO_RETRY, + Dispatchers.IO.asExecutor(), + signal, + callback + ) + } + } else { + runOnIoDispatcher { + // 老版本系统,继续用阻塞的 InetAddress + try { + val u = SagerNet.underlyingNetwork + val answer = try { + u?.getAllByName(domain) + } catch (e: UnknownHostException) { + null + } ?: InetAddress.getAllByName(domain) + if (answer != null) { + ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n")) } else { - DnsResolver.getInstance().query( - SagerNet.underlyingNetwork, - domain, - DnsResolver.FLAG_EMPTY, - Dispatchers.IO.asExecutor(), - signal, - callback - ) + ctx.errnoCode(114514) } + } catch (e: UnknownHostException) { + ctx.errorCode(RCODE_NXDOMAIN) + } catch (e: Exception) { + Logs.w(e) + ctx.errnoCode(114514) } } - } else { - throw Exception("114514") } } diff --git a/app/src/main/java/moe/matsuri/nb4a/plugin/NekoPluginManager.kt b/app/src/main/java/moe/matsuri/nb4a/plugin/NekoPluginManager.kt deleted file mode 100644 index d7ae50e10..000000000 --- a/app/src/main/java/moe/matsuri/nb4a/plugin/NekoPluginManager.kt +++ /dev/null @@ -1,153 +0,0 @@ -package moe.matsuri.nb4a.plugin - -import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.SagerNet -import io.nekohasekai.sagernet.bg.BaseService -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.ktx.forEach -import io.nekohasekai.sagernet.utils.PackageCache -import moe.matsuri.nb4a.proxy.neko.NekoJSInterface -import okhttp3.internal.closeQuietly -import org.json.JSONObject -import java.io.File -import java.util.zip.CRC32 -import java.util.zip.ZipFile - -object NekoPluginManager { - const val managerVersion = 2 - - val plugins get() = DataStore.nekoPlugins.split("\n").filter { it.isNotBlank() } - - // plgID to plgConfig object - fun getManagedPlugins(): Map { - val ret = mutableMapOf() - plugins.forEach { - tryGetPlgConfig(it)?.apply { - ret[it] = this - } - } - return ret - } - - class Protocol( - val protocolId: String, val plgId: String, val protocolConfig: JSONObject - ) - - fun getProtocols(): List { - val ret = mutableListOf() - getManagedPlugins().forEach { (t, u) -> - u.optJSONArray("protocols")?.forEach { _, any -> - if (any is JSONObject) { - val name = any.optString("protocolId") - ret.add(Protocol(name, t, any)) - } - } - } - return ret - } - - fun findProtocol(protocolId: String): Protocol? { - getManagedPlugins().forEach { (t, u) -> - u.optJSONArray("protocols")?.forEach { _, any -> - if (any is JSONObject) { - if (protocolId == any.optString("protocolId")) { - return Protocol(protocolId, t, any) - } - } - } - } - return null - } - - fun removeManagedPlugin(plgId: String) { - DataStore.configurationStore.remove(plgId) - val dir = File(SagerNet.application.filesDir.absolutePath + "/plugins/" + plgId) - if (dir.exists()) { - dir.deleteRecursively() - } - } - - fun extractPlugin(plgId: String, install: Boolean) { - val app = PackageCache.installedApps[plgId] ?: return - val apk = File(app.publicSourceDir) - if (!apk.exists()) { - return - } - if (!install && !plugins.contains(plgId)) { - return - } - - val zipFile = ZipFile(apk) - val unzipDir = File(SagerNet.application.filesDir.absolutePath + "/plugins/" + plgId) - unzipDir.mkdirs() - for (entry in zipFile.entries()) { - if (entry.name.startsWith("assets/")) { - val relativePath = entry.name.removePrefix("assets/") - val outFile = File(unzipDir, relativePath) - if (entry.isDirectory) { - outFile.mkdirs() - continue - } - - if (outFile.isDirectory) { - outFile.delete() - } else if (outFile.exists()) { - val checksum = CRC32() - checksum.update(outFile.readBytes()) - if (checksum.value == entry.crc) { - continue - } - } - - val input = zipFile.getInputStream(entry) - outFile.outputStream().use { - input.copyTo(it) - } - } - } - zipFile.closeQuietly() - } - - suspend fun installPlugin(plgId: String) { - if (plgId == "moe.matsuri.plugin.singbox" || plgId == "moe.matsuri.plugin.xray") { - throw Exception("This plugin is deprecated") - } - extractPlugin(plgId, true) - NekoJSInterface.Default.destroyJsi(plgId) - NekoJSInterface.Default.requireJsi(plgId).init() - NekoJSInterface.Default.destroyJsi(plgId) - } - - const val PLUGIN_APP_VERSION = "_v_vc" - const val PLUGIN_APP_VERSION_NAME = "_v_vn" - - // Return null if not managed - fun tryGetPlgConfig(plgId: String): JSONObject? { - return try { - JSONObject(DataStore.configurationStore.getString(plgId)!!) - } catch (e: Exception) { - null - } - } - - fun updatePlgConfig(plgId: String, plgConfig: JSONObject) { - PackageCache.installedPluginPackages[plgId]?.apply { - // longVersionCode requires API 28 -// plgConfig.put(PLUGIN_APP_VERSION, versionCode) - plgConfig.put(PLUGIN_APP_VERSION_NAME, versionName) - } - DataStore.configurationStore.putString(plgId, plgConfig.toString()) - } - - fun htmlPath(plgId: String): String { - val htmlFile = File(SagerNet.application.filesDir.absolutePath + "/plugins/" + plgId) - return htmlFile.absolutePath - } - - class PluginInternalException(val protocolId: String) : Exception(), - BaseService.ExpectedException { - override fun getLocalizedMessage() = - SagerNet.application.getString(R.string.neko_plugin_internal_error, protocolId) - } - -} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt b/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt index 677710d4b..a584cadfa 100644 --- a/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt +++ b/app/src/main/java/moe/matsuri/nb4a/plugin/Plugins.kt @@ -14,20 +14,18 @@ import io.nekohasekai.sagernet.utils.PackageCache object Plugins { const val AUTHORITIES_PREFIX_SEKAI_EXE = "io.nekohasekai.sagernet.plugin." const val AUTHORITIES_PREFIX_NEKO_EXE = "moe.matsuri.exe." - const val AUTHORITIES_PREFIX_NEKO_PLUGIN = "moe.matsuri.plugin." const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id" const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path" - fun isExeOrPlugin(pkg: PackageInfo): Boolean { - if (pkg.providers == null || pkg.providers.isEmpty()) return false - val provider = pkg.providers[0] ?: return false + fun isExe(pkg: PackageInfo): Boolean { + if (pkg.providers?.isEmpty() == true) return false + val provider = pkg.providers?.get(0) ?: return false val auth = provider.authority ?: return false return auth.startsWith(AUTHORITIES_PREFIX_SEKAI_EXE) || auth.startsWith(AUTHORITIES_PREFIX_NEKO_EXE) - || auth.startsWith(AUTHORITIES_PREFIX_NEKO_PLUGIN) } fun preferExePrefix(): String { @@ -55,12 +53,19 @@ object Plugins { fun getPlugin(pluginId: String): ProviderInfo? { if (pluginId.isBlank()) return null + getPluginExternal(pluginId)?.let { return it } + // internal so + return ProviderInfo().apply { authority = AUTHORITIES_PREFIX_NEKO_EXE } + } + + fun getPluginExternal(pluginId: String): ProviderInfo? { + if (pluginId.isBlank()) return null // try queryIntentContentProviders - var providers = getPluginOld(pluginId) + var providers = getExtPluginOld(pluginId) // try PackageCache - if (providers.isEmpty()) providers = getPluginNew(pluginId) + if (providers.isEmpty()) providers = getExtPluginNew(pluginId) // not found if (providers.isEmpty()) return null @@ -81,12 +86,12 @@ object Plugins { return providers[0] } - fun getPluginNew(pluginId: String): List { + private fun getExtPluginNew(pluginId: String): List { PackageCache.awaitLoadSync() val pkgs = PackageCache.installedPluginPackages .map { it.value } - .filter { it.providers[0].loadString(METADATA_KEY_ID) == pluginId } - return pkgs.map { it.providers[0] } + .filter { it.providers?.get(0)?.loadString(METADATA_KEY_ID) == pluginId } + return pkgs.mapNotNull { it.providers?.get(0) } } private fun buildUri(id: String, auth: String) = Uri.Builder() @@ -95,7 +100,7 @@ object Plugins { .path("/$id") .build() - private fun getPluginOld(pluginId: String): List { + private fun getExtPluginOld(pluginId: String): List { var flags = PackageManager.GET_META_DATA if (Build.VERSION.SDK_INT >= 24) { flags = diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSBean.java b/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSBean.java new file mode 100644 index 000000000..43f4144c0 --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSBean.java @@ -0,0 +1,82 @@ +package moe.matsuri.nb4a.proxy.anytls; + +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 AnyTLSBean extends AbstractBean { + + public static final Creator CREATOR = new CREATOR() { + @NonNull + @Override + public AnyTLSBean newInstance() { + return new AnyTLSBean(); + } + + @Override + public AnyTLSBean[] newArray(int size) { + return new AnyTLSBean[size]; + } + }; + public String password; + public String sni; + public String alpn; + public String certificates; + public String utlsFingerprint; + public Boolean allowInsecure; + // In sing-box, this seemed can be used with REALITY. + // But even mihomo appended many options, it still not provide REALITY. + // https://github.com/anytls/anytls-go/blob/4636d90462fa21a510420512d7706a9acf69c7b9/docs/faq.md?plain=1#L25-L37 + + public String echConfig; + + @Override + public void initializeDefaultValues() { + super.initializeDefaultValues(); + if (password == null) password = ""; + if (sni == null) sni = ""; + if (alpn == null) alpn = ""; + if (certificates == null) certificates = ""; + if (utlsFingerprint == null) utlsFingerprint = ""; + if (allowInsecure == null) allowInsecure = false; + if (echConfig == null) echConfig = ""; + } + + @Override + public void serialize(ByteBufferOutput output) { + output.writeInt(0); + super.serialize(output); + output.writeString(password); + output.writeString(sni); + output.writeString(alpn); + output.writeString(certificates); + output.writeString(utlsFingerprint); + output.writeBoolean(allowInsecure); + output.writeString(echConfig); + } + + @Override + public void deserialize(ByteBufferInput input) { + int version = input.readInt(); + super.deserialize(input); + password = input.readString(); + sni = input.readString(); + alpn = input.readString(); + certificates = input.readString(); + utlsFingerprint = input.readString(); + allowInsecure = input.readBoolean(); + echConfig = input.readString(); + } + + @NotNull + @Override + public AnyTLSBean clone() { + return KryoConverters.deserialize(new AnyTLSBean(), KryoConverters.serialize(this)); + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSFmt.kt b/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSFmt.kt new file mode 100644 index 000000000..2fb7b4513 --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSFmt.kt @@ -0,0 +1,81 @@ +package moe.matsuri.nb4a.proxy.anytls + +import io.nekohasekai.sagernet.ktx.blankAsNull +import io.nekohasekai.sagernet.ktx.linkBuilder +import io.nekohasekai.sagernet.ktx.toLink +import io.nekohasekai.sagernet.ktx.urlSafe +import moe.matsuri.nb4a.SingBoxOptions +import moe.matsuri.nb4a.utils.listByLineOrComma +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +fun buildSingBoxOutboundAnyTLSBean(bean: AnyTLSBean): SingBoxOptions.Outbound_AnyTLSOptions { + return SingBoxOptions.Outbound_AnyTLSOptions().apply { + type = "anytls" + server = bean.serverAddress + server_port = bean.serverPort + password = bean.password + + tls = SingBoxOptions.OutboundTLSOptions().apply { + enabled = true + server_name = bean.sni.blankAsNull() + if (bean.allowInsecure) insecure = true + alpn = bean.alpn.blankAsNull()?.listByLineOrComma() + bean.certificates.blankAsNull()?.let { + certificate = it + } + bean.utlsFingerprint.blankAsNull()?.let { + utls = SingBoxOptions.OutboundUTLSOptions().apply { + enabled = true + fingerprint = it + } + } + bean.echConfig.blankAsNull()?.let { + // In new version, some complex options will be deprecated, so we just do this. + ech = SingBoxOptions.OutboundECHOptions().apply { + enabled = true + config = listOf(it) + } + } + } + } +} + +fun AnyTLSBean.toUri(): String { + val builder = linkBuilder() + .host(serverAddress) + .port(serverPort) + .username(password) + if (!name.isNullOrBlank()) { + builder.encodedFragment(name.urlSafe()) + } + if (allowInsecure) { + builder.addQueryParameter("insecure", "1") + } + if (!sni.isNullOrBlank()) { + builder.addQueryParameter("sni", sni) + } + if (!utlsFingerprint.isNullOrBlank()) { + builder.addQueryParameter("fp", utlsFingerprint) + } + return builder.toLink("anytls") +} + +fun parseAnytls(url: String): AnyTLSBean { + // https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md + val link = url.replace("anytls://", "https://").toHttpUrlOrNull() ?: error( + "invalid anytls link $url" + ) + return AnyTLSBean().apply { + serverAddress = link.host + serverPort = link.port + name = link.fragment + password = link.username + sni = link.queryParameter("sni") ?: "" + link.queryParameter("insecure")?.also { + allowInsecure = it == "1" || it == "true" + } + link.queryParameter("fp")?.let { + utlsFingerprint = it + } + } +} diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSSettingsActivity.kt b/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSSettingsActivity.kt new file mode 100644 index 000000000..13d256d1b --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/proxy/anytls/AnyTLSSettingsActivity.kt @@ -0,0 +1,51 @@ +package moe.matsuri.nb4a.proxy.anytls + +import android.os.Bundle +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceFragmentCompat +import io.nekohasekai.sagernet.Key +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers +import io.nekohasekai.sagernet.ktx.applyDefaultValues +import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity +import moe.matsuri.nb4a.proxy.PreferenceBinding +import moe.matsuri.nb4a.proxy.PreferenceBindingManager +import moe.matsuri.nb4a.proxy.Type + +class AnyTLSSettingsActivity : ProfileSettingsActivity() { + override fun createEntity() = AnyTLSBean().applyDefaultValues() + + private val pbm = PreferenceBindingManager() + private val name = pbm.add(PreferenceBinding(Type.Text, "name")) + private val serverAddress = pbm.add(PreferenceBinding(Type.Text, "serverAddress")) + private val serverPort = pbm.add(PreferenceBinding(Type.TextToInt, "serverPort")) + private val password = pbm.add(PreferenceBinding(Type.Text, "password")) + private val sni = pbm.add(PreferenceBinding(Type.Text, "sni")) + private val alpn = pbm.add(PreferenceBinding(Type.Text, "alpn")) + private val certificates = pbm.add(PreferenceBinding(Type.Text, "certificates")) + private val allowInsecure = pbm.add(PreferenceBinding(Type.Bool, "allowInsecure")) + private val utlsFingerprint = pbm.add(PreferenceBinding(Type.Text, "utlsFingerprint")) + + override fun AnyTLSBean.init() { + pbm.writeToCacheAll(this) + + } + + override fun AnyTLSBean.serialize() { + pbm.fromCacheAll(this) + } + + override fun PreferenceFragmentCompat.createPreferences( + savedInstanceState: Bundle?, + rootKey: String? + ) { + addPreferencesFromResource(R.xml.anytls_preferences) + + findPreference(Key.SERVER_PORT)!!.apply { + setOnBindEditTextListener(EditTextPreferenceModifiers.Port) + } + findPreference("password")!!.apply { + summaryProvider = PasswordSummaryProvider + } + } +} diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java index 3a86b7fce..434d54923 100644 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java +++ b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoBean.java @@ -8,18 +8,12 @@ import org.jetbrains.annotations.NotNull; import org.json.JSONObject; -import io.nekohasekai.sagernet.R; -import io.nekohasekai.sagernet.SagerNet; import io.nekohasekai.sagernet.fmt.AbstractBean; import io.nekohasekai.sagernet.fmt.KryoConverters; import io.nekohasekai.sagernet.ktx.Logs; -import moe.matsuri.nb4a.plugin.NekoPluginManager; public class NekoBean extends AbstractBean { - // BoxInstance use this - public JSONObject allConfig = null; - public String plgId; public String protocolId; public JSONObject sharedStorage = new JSONObject(); @@ -62,31 +56,22 @@ public static JSONObject tryParseJSON(String input) { } public String displayType() { - NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId); - String neko = SagerNet.application.getResources().getString(R.string.neko_plugin); - if (p == null) return neko; - return p.getProtocolId(); + return "invalid"; } @Override public boolean canMapping() { - NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId); - if (p == null) return false; - return p.getProtocolConfig().optBoolean("canMapping"); + return false; } @Override public boolean canICMPing() { - NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId); - if (p == null) return false; - return p.getProtocolConfig().optBoolean("canICMPing"); + return false; } @Override public boolean canTCPing() { - NekoPluginManager.Protocol p = NekoPluginManager.INSTANCE.findProtocol(protocolId); - if (p == null) return false; - return p.getProtocolConfig().optBoolean("canTCPing"); + return false; } @NotNull diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoFmt.kt b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoFmt.kt deleted file mode 100644 index 88e45ec58..000000000 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoFmt.kt +++ /dev/null @@ -1,123 +0,0 @@ -package moe.matsuri.nb4a.proxy.neko - -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.ktx.Logs -import io.nekohasekai.sagernet.ktx.getStr -import io.nekohasekai.sagernet.ktx.runOnIoDispatcher -import libcore.Libcore -import moe.matsuri.nb4a.Protocols -import moe.matsuri.nb4a.plugin.NekoPluginManager -import org.json.JSONObject -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -suspend fun parseShareLink(plgId: String, protocolId: String, link: String): NekoBean = - suspendCoroutine { - runOnIoDispatcher { - val jsi = NekoJSInterface.Default.requireJsi(plgId) - jsi.lock() - - try { - jsi.init() - - val jsip = jsi.switchProtocol(protocolId) - val sharedStorage = jsip.parseShareLink(link) - - // NekoBean from link - val bean = NekoBean() - bean.plgId = plgId - bean.protocolId = protocolId - bean.sharedStorage = NekoBean.tryParseJSON(sharedStorage) - bean.onSharedStorageSet() - - it.resume(bean) - } catch (e: Exception) { - Logs.e(e) - it.resume(NekoBean().apply { - this.plgId = plgId - this.protocolId = protocolId - }) - } - - jsi.unlock() - // destroy when all link parsed - } - } - -fun NekoBean.shareLink(): String { - return sharedStorage.optString("shareLink") -} - -// Only run in bg process -// seems no concurrent -suspend fun NekoBean.updateAllConfig(port: Int) = suspendCoroutine { - allConfig = null - - runOnIoDispatcher { - val jsi = NekoJSInterface.Default.requireJsi(plgId) - jsi.lock() - - try { - jsi.init() - val jsip = jsi.switchProtocol(protocolId) - - // runtime arguments - val otherArgs = mutableMapOf() - otherArgs["finalAddress"] = finalAddress - otherArgs["finalPort"] = finalPort - otherArgs["muxEnabled"] = Protocols.shouldEnableMux(protocolId) - otherArgs["muxConcurrency"] = DataStore.muxConcurrency - - val ret = jsip.buildAllConfig(port, this@updateAllConfig, otherArgs) - - // result - allConfig = JSONObject(ret) - } catch (e: Exception) { - Logs.e(e) - } - - jsi.unlock() - it.resume(Unit) - // destroy when config generated / all tests finished - } -} - -fun NekoBean.cacheGet(id: String): String? { - return DataStore.profileCacheStore.getString("neko_${hash()}_$id") -} - -fun NekoBean.cacheSet(id: String, value: String) { - DataStore.profileCacheStore.putString("neko_${hash()}_$id", value) -} - -fun NekoBean.hash(): String { - var a = plgId - a += protocolId - a += sharedStorage.toString() - return Libcore.sha256Hex(a.toByteArray()) -} - -// must call it to update something like serverAddress -fun NekoBean.onSharedStorageSet() { - serverAddress = sharedStorage.getStr("serverAddress") - serverPort = sharedStorage.getStr("serverPort")?.toInt() ?: 1080 - if (serverAddress == null || serverAddress.isBlank()) { - serverAddress = "127.0.0.1" - } - name = sharedStorage.optString("name") -} - -fun NekoBean.needBypassRootUid(): Boolean { - val p = NekoPluginManager.findProtocol(protocolId) ?: return false - return p.protocolConfig.optBoolean("needBypassRootUid") -} - -fun NekoBean.haveStandardLink(): Boolean { - val p = NekoPluginManager.findProtocol(protocolId) ?: return false - return p.protocolConfig.optBoolean("haveStandardLink") -} - -fun NekoBean.canShare(): Boolean { - val p = NekoPluginManager.findProtocol(protocolId) ?: return false - return p.protocolConfig.optBoolean("canShare") -} diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoJSInterface.kt b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoJSInterface.kt deleted file mode 100644 index df165c75f..000000000 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoJSInterface.kt +++ /dev/null @@ -1,388 +0,0 @@ -package moe.matsuri.nb4a.proxy.neko - -import android.annotation.SuppressLint -import android.webkit.* -import android.widget.Toast -import androidx.preference.Preference -import androidx.preference.PreferenceScreen -import io.nekohasekai.sagernet.BuildConfig -import io.nekohasekai.sagernet.SagerNet -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.ktx.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withContext -import moe.matsuri.nb4a.plugin.NekoPluginManager -import moe.matsuri.nb4a.ui.SimpleMenuPreference -import moe.matsuri.nb4a.utils.JavaUtil -import moe.matsuri.nb4a.utils.Util -import moe.matsuri.nb4a.utils.WebViewUtil -import org.json.JSONObject -import java.io.File -import java.io.FileInputStream -import java.util.* -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -class NekoJSInterface(val plgId: String) { - - private val mutex = Mutex() - private var webView: WebView? = null - val jsObject = JsObject() - var plgConfig: JSONObject? = null - var plgConfigException: Exception? = null - val protocols = mutableMapOf() - val loaded = AtomicBoolean() - - suspend fun lock() { - mutex.lock(null) - } - - fun unlock() { - mutex.unlock(null) - } - - // load webview and js - // Return immediately when already loaded - // Return plgConfig or throw exception - suspend fun init() = withContext(Dispatchers.Main) { - initInternal() - } - - @SuppressLint("SetJavaScriptEnabled") - private suspend fun initInternal() = suspendCoroutine { - if (loaded.get()) { - plgConfig?.apply { - it.resume(this) - return@suspendCoroutine - } - plgConfigException?.apply { - it.resumeWithException(this) - return@suspendCoroutine - } - it.resumeWithException(Exception("wtf")) - return@suspendCoroutine - } - - WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) - NekoPluginManager.extractPlugin(plgId, false) - - webView = WebView(SagerNet.application.applicationContext) - webView!!.settings.javaScriptEnabled = true - webView!!.addJavascriptInterface(jsObject, "neko") - webView!!.webViewClient = object : WebViewClient() { - // provide files - override fun shouldInterceptRequest( - view: WebView?, request: WebResourceRequest? - ): WebResourceResponse { - return WebViewUtil.interceptRequest( - { res -> - val f = File(NekoPluginManager.htmlPath(plgId), res) - if (f.exists()) { - FileInputStream(f) - } else { - null - } - }, - view, - request - ) - } - - override fun onReceivedError( - view: WebView?, request: WebResourceRequest?, error: WebResourceError? - ) { - WebViewUtil.onReceivedError(view, request, error) - } - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - if (loaded.getAndSet(true)) return - - runOnIoDispatcher { - // Process nekoInit - var ret = "" - try { - ret = nekoInit() - val obj = JSONObject(ret) - if (!obj.getBoolean("ok")) { - throw Exception("plugin refuse to run: ${obj.optString("reason")}") - } - val min = obj.getInt("minVersion") - if (min > NekoPluginManager.managerVersion) { - throw Exception("manager version ${NekoPluginManager.managerVersion} too old, this plugin requires >= $min") - } - plgConfig = obj - NekoPluginManager.updatePlgConfig(plgId, obj) - it.resume(obj) - } catch (e: Exception) { - val e2 = Exception("nekoInit: " + e.readableMessage + "\n\n" + ret) - plgConfigException = e2 - it.resumeWithException(e2) - } - } - } - } - webView!!.loadUrl("http://$plgId/plugin.html") - } - - // Android call JS - - private suspend fun callJS(script: String): String = suspendCoroutine { - val jsLatch = CountDownLatch(1) - var jsReceivedValue = "" - - runOnMainDispatcher { - if (webView != null) { - webView!!.evaluateJavascript(script) { value -> - jsReceivedValue = value - jsLatch.countDown() - } - } else { - jsReceivedValue = "webView is null" - jsLatch.countDown() - } - } - - jsLatch.await(5, TimeUnit.SECONDS) - - // evaluateJavascript escapes Javascript's String - jsReceivedValue = JavaUtil.unescapeString(jsReceivedValue.removeSurrounding("\"")) - if (BuildConfig.DEBUG) Logs.d("$script: $jsReceivedValue") - it.resume(jsReceivedValue) - } - - // call once - private suspend fun nekoInit(): String { - val sendData = JSONObject() - sendData.put("lang", Locale.getDefault().toLanguageTag()) - sendData.put("plgId", plgId) - sendData.put("managerVersion", NekoPluginManager.managerVersion) - - return callJS( - "nekoInit(\"${ - Util.b64EncodeUrlSafe( - sendData.toString().toByteArray() - ) - }\")" - ) - } - - fun switchProtocol(id: String): NekoProtocol { - lateinit var p: NekoProtocol - if (protocols.containsKey(id)) { - p = protocols[id]!! - } else { - p = NekoProtocol(id) { callJS(it) } - protocols[id] = p - } - jsObject.protocol = p - return p - } - - suspend fun getAbout(): String { - return callJS("nekoAbout()") - } - - inner class NekoProtocol(val protocolId: String, val callJS: suspend (String) -> String) { - private suspend fun callProtocol(method: String, b64Str: String?): String { - var arg = "" - if (b64Str != null) { - arg = "\"" + b64Str + "\"" - } - return callJS("nekoProtocol(\"$protocolId\").$method($arg)") - } - - suspend fun buildAllConfig( - port: Int, bean: NekoBean, otherArgs: Map? - ): String { - val sendData = JSONObject() - sendData.put("port", port) - sendData.put( - "sharedStorage", - Util.b64EncodeUrlSafe(bean.sharedStorage.toString().toByteArray()) - ) - otherArgs?.forEach { (t, u) -> sendData.put(t, u) } - - return callProtocol( - "buildAllConfig", Util.b64EncodeUrlSafe(sendData.toString().toByteArray()) - ) - } - - suspend fun parseShareLink(shareLink: String): String { - val sendData = JSONObject() - sendData.put("shareLink", shareLink) - - return callProtocol( - "parseShareLink", Util.b64EncodeUrlSafe(sendData.toString().toByteArray()) - ) - } - - // UI Interface - - suspend fun setSharedStorage(sharedStorage: String) { - callProtocol( - "setSharedStorage", - Util.b64EncodeUrlSafe(sharedStorage.toByteArray()) - ) - } - - suspend fun requireSetProfileCache() { - callProtocol("requireSetProfileCache", null) - } - - suspend fun requirePreferenceScreenConfig(): String { - return callProtocol("requirePreferenceScreenConfig", null) - } - - suspend fun sharedStorageFromProfileCache(): String { - return callProtocol("sharedStorageFromProfileCache", null) - } - - suspend fun onPreferenceCreated() { - callProtocol("onPreferenceCreated", null) - } - - suspend fun onPreferenceChanged(key: String, v: Any) { - val sendData = JSONObject() - sendData.put("key", key) - sendData.put("newValue", v) - - callProtocol( - "onPreferenceChanged", - Util.b64EncodeUrlSafe(sendData.toString().toByteArray()) - ) - } - - } - - inner class JsObject { - var preferenceScreen: PreferenceScreen? = null - var protocol: NekoProtocol? = null - - // JS call Android - - @JavascriptInterface - fun toast(s: String) { - Toast.makeText(SagerNet.application.applicationContext, s, Toast.LENGTH_SHORT).show() - } - - @JavascriptInterface - fun logError(s: String) { - Logs.e("logError: $s") - } - - @JavascriptInterface - fun setPreferenceVisibility(key: String, isVisible: Boolean) { - runBlockingOnMainDispatcher { - preferenceScreen?.findPreference(key)?.isVisible = isVisible - } - } - - @JavascriptInterface - fun setPreferenceTitle(key: String, title: String) { - runBlockingOnMainDispatcher { - preferenceScreen?.findPreference(key)?.title = title - } - } - - @JavascriptInterface - fun setMenu(key: String, entries: String) { - runBlockingOnMainDispatcher { - preferenceScreen?.findPreference(key)?.apply { - NekoPreferenceInflater.setMenu(this, JSONObject(entries)) - } - } - } - - @JavascriptInterface - fun listenOnPreferenceChanged(key: String) { - preferenceScreen?.findPreference(key) - ?.setOnPreferenceChangeListener { preference, newValue -> - runOnIoDispatcher { - protocol?.onPreferenceChanged(preference.key, newValue) - } - true - } - } - - @JavascriptInterface - fun setKV(type: Int, key: String, jsonStr: String) { - try { - val v = JSONObject(jsonStr) - when (type) { - 0 -> DataStore.profileCacheStore.putBoolean(key, v.getBoolean("v")) - 1 -> DataStore.profileCacheStore.putFloat(key, v.getDouble("v").toFloat()) - 2 -> DataStore.profileCacheStore.putInt(key, v.getInt("v")) - 3 -> DataStore.profileCacheStore.putLong(key, v.getLong("v")) - 4 -> DataStore.profileCacheStore.putString(key, v.getString("v")) - } - } catch (e: Exception) { - Logs.e("setKV: $e") - } - } - - @JavascriptInterface - fun getKV(type: Int, key: String): String { - val v = JSONObject() - try { - when (type) { - 0 -> v.put("v", DataStore.profileCacheStore.getBoolean(key)) - 1 -> v.put("v", DataStore.profileCacheStore.getFloat(key)) - 2 -> v.put("v", DataStore.profileCacheStore.getInt(key)) - 3 -> v.put("v", DataStore.profileCacheStore.getLong(key)) - 4 -> v.put("v", DataStore.profileCacheStore.getString(key)) - } - } catch (e: Exception) { - Logs.e("getKV: $e") - } - return v.toString() - } - - } - - fun destroy() { - webView?.onPause() - webView?.removeAllViews() - webView?.destroy() - webView = null - } - - suspend fun destorySuspend() = withContext(Dispatchers.Main) { - destroy() - } - - object Default { - val map = mutableMapOf() - - suspend fun destroyJsi(plgId: String) = withContext(Dispatchers.Main) { - if (map.containsKey(plgId)) { - map[plgId]!!.destroy() - map.remove(plgId) - } - } - - // now it's manually managed - suspend fun destroyAllJsi() = withContext(Dispatchers.Main) { - map.forEach { (t, u) -> - u.destroy() - map.remove(t) - } - } - - suspend fun requireJsi(plgId: String): NekoJSInterface = withContext(Dispatchers.Main) { - lateinit var jsi: NekoJSInterface - if (map.containsKey(plgId)) { - jsi = map[plgId]!! - } else { - jsi = NekoJSInterface(plgId) - map[plgId] = jsi - } - return@withContext jsi - } - } -} diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoPreferenceInflater.kt b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoPreferenceInflater.kt deleted file mode 100644 index cd2fdff2e..000000000 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoPreferenceInflater.kt +++ /dev/null @@ -1,97 +0,0 @@ -package moe.matsuri.nb4a.proxy.neko - -import androidx.preference.* -import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers -import io.nekohasekai.sagernet.ktx.forEach -import io.nekohasekai.sagernet.ktx.getStr -import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import moe.matsuri.nb4a.ui.SimpleMenuPreference -import moe.matsuri.nb4a.utils.getDrawableByName -import org.json.JSONArray -import org.json.JSONObject - -object NekoPreferenceInflater { - suspend fun inflate(pref: JSONArray, preferenceScreen: PreferenceScreen) = - withContext(Dispatchers.Main) { - val context = preferenceScreen.context - pref.forEach { _, category -> - category as JSONObject - - val preferenceCategory = PreferenceCategory(context) - preferenceScreen.addPreference(preferenceCategory) - - category.getStr("key")?.apply { preferenceCategory.key = this } - category.getStr("title")?.apply { preferenceCategory.title = this } - - category.optJSONArray("preferences")?.forEach { _, any -> - if (any is JSONObject) { - lateinit var p: Preference - // Create Preference - when (any.getStr("type")) { - "EditTextPreference" -> { - p = EditTextPreference(context).apply { - when (any.getStr("summaryProvider")) { - null -> summaryProvider = - EditTextPreference.SimpleSummaryProvider.getInstance() - "PasswordSummaryProvider" -> summaryProvider = - ProfileSettingsActivity.PasswordSummaryProvider - } - when (any.getStr("EditTextPreferenceModifiers")) { - "Monospace" -> setOnBindEditTextListener( - EditTextPreferenceModifiers.Monospace - ) - "Hosts" -> setOnBindEditTextListener( - EditTextPreferenceModifiers.Hosts - ) - "Port" -> setOnBindEditTextListener( - EditTextPreferenceModifiers.Port - ) - "Number" -> setOnBindEditTextListener( - EditTextPreferenceModifiers.Number - ) - } - } - } - "SwitchPreference" -> { - p = SwitchPreference(context) - } - "SimpleMenuPreference" -> { - p = SimpleMenuPreference(context).apply { - val entries = any.optJSONObject("entries") - if (entries != null) setMenu(this, entries) - } - } - } - // Set key & title - p.key = any.getString("key") - any.getStr("title")?.apply { p.title = this } - // Set icon - any.getStr("icon")?.apply { - p.icon = context.getDrawableByName(this) - } - // Set summary - any.getStr("summary")?.apply { - p.summary = this - } - // Add to category - preferenceCategory.addPreference(p) - } - } - } - } - - fun setMenu(p: SimpleMenuPreference, entries: JSONObject) { - val menuEntries = mutableListOf() - val menuEntryValues = mutableListOf() - entries.forEach { s, b -> - menuEntryValues.add(s) - menuEntries.add(b as String) - } - entries.apply { - p.entries = menuEntries.toTypedArray() - p.entryValues = menuEntryValues.toTypedArray() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoSettingActivity.kt b/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoSettingActivity.kt deleted file mode 100644 index e167fd6e4..000000000 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/neko/NekoSettingActivity.kt +++ /dev/null @@ -1,102 +0,0 @@ -package moe.matsuri.nb4a.proxy.neko - -import android.os.Bundle -import android.view.View -import androidx.core.view.isVisible -import androidx.preference.PreferenceDataStore -import androidx.preference.PreferenceFragmentCompat -import io.nekohasekai.sagernet.Key -import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.ktx.runOnIoDispatcher -import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity -import moe.matsuri.nb4a.ui.Dialogs -import org.json.JSONArray - -class NekoSettingActivity : ProfileSettingsActivity() { - - lateinit var jsi: NekoJSInterface - lateinit var jsip: NekoJSInterface.NekoProtocol - lateinit var plgId: String - lateinit var protocolId: String - var loaded = false - - override fun createEntity() = NekoBean() - - override fun NekoBean.init() { - if (!this@NekoSettingActivity::plgId.isInitialized) this@NekoSettingActivity.plgId = plgId - if (!this@NekoSettingActivity::protocolId.isInitialized) this@NekoSettingActivity.protocolId = protocolId - DataStore.profileCacheStore.putString("name", name) - DataStore.sharedStorage = sharedStorage.toString() - } - - override fun NekoBean.serialize() { - // NekoBean from input - plgId = this@NekoSettingActivity.plgId - protocolId = this@NekoSettingActivity.protocolId - - sharedStorage = NekoBean.tryParseJSON(DataStore.sharedStorage) - onSharedStorageSet() - } - - override fun onCreate(savedInstanceState: Bundle?) { - intent?.getStringExtra("plgId")?.apply { plgId = this } - intent?.getStringExtra("protocolId")?.apply { protocolId = this } - super.onCreate(savedInstanceState) - } - - override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) { - listView.isVisible = false - } - - override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { - if (loaded && key != Key.PROFILE_DIRTY) { - DataStore.dirty = true - } - } - - override fun PreferenceFragmentCompat.createPreferences( - savedInstanceState: Bundle?, - rootKey: String?, - ) { - addPreferencesFromResource(R.xml.neko_preferences) - - // Create a jsi - jsi = NekoJSInterface(plgId) - runOnIoDispatcher { - try { - jsi.init() - jsip = jsi.switchProtocol(protocolId) - jsi.jsObject.preferenceScreen = preferenceScreen - - // Because of the Preference problem, first require the KV and then inflate the UI - jsip.setSharedStorage(DataStore.sharedStorage) - jsip.requireSetProfileCache() - - val config = jsip.requirePreferenceScreenConfig() - val pref = JSONArray(config) - - NekoPreferenceInflater.inflate(pref, preferenceScreen) - jsip.onPreferenceCreated() - - runOnUiThread { - loaded = true - listView.isVisible = true - } - } catch (e: Exception) { - Dialogs.logExceptionAndShow(this@NekoSettingActivity, e) { finish() } - } - } - } - - override suspend fun saveAndExit() { - DataStore.sharedStorage = jsip.sharedStorageFromProfileCache() - super.saveAndExit() // serialize & finish - } - - override fun onDestroy() { - jsi.destroy() - super.onDestroy() - } - -} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt index ee0728edf..da87e9c24 100644 --- a/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt +++ b/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt @@ -20,7 +20,6 @@ import androidx.preference.PreferenceViewHolder import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.ktx.getColorAttr -import io.nekohasekai.sagernet.ktx.isExpertFlavor import kotlin.math.roundToInt class ColorPickerPreference @@ -91,7 +90,6 @@ class ColorPickerPreference for (color in colors) { i++ //Theme.kt - if (!isExpertFlavor && i in listOf(21)) continue val themeId = i val view = getNekoImageViewAtColor(color, 64, 0).apply { @@ -117,4 +115,4 @@ class ColorPickerPreference .setNegativeButton(android.R.string.cancel, null) .show() } -} \ No newline at end of file +} diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt new file mode 100644 index 000000000..31bc7a193 --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt @@ -0,0 +1,29 @@ +package moe.matsuri.nb4a.ui + +import android.content.Context +import androidx.core.app.NotificationCompat +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.ktx.Logs + +class ConnectionTestNotification(val context: Context, val title: String) { + private val channelId = "connection-test" + private val notificationId = 1001 + + fun updateNotification(progress: Int, max: Int, finished: Boolean) { + try { + if (finished) { + SagerNet.notification.cancel(notificationId) + return + } + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_service_active) + .setContentTitle(title) + .setOnlyAlertOnce(true) + .setContentText("$progress / $max").setProgress(max, progress, false) + SagerNet.notification.notify(notificationId, builder.build()) + } catch (e: Exception) { + Logs.w(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt index 6b2ea819a..1292ed5c5 100644 --- a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt +++ b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt @@ -4,8 +4,10 @@ import android.content.Context import android.content.Intent import android.util.AttributeSet import androidx.preference.Preference +import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity @@ -26,9 +28,27 @@ class EditConfigPreference : Preference { intent = Intent(context, ConfigEditActivity::class.java) } + var configKey = Key.SERVER_CONFIG + var useConfigStore = false + + fun useConfigStore(key: String) { + try { + this.configKey = key + useConfigStore = true + intent = intent!!.apply { + putExtra("useConfigStore", "1") + putExtra("key", key) + } + } catch (e: Exception) { + Logs.w(e) + } + } + override fun getSummary(): CharSequence { - val config = DataStore.serverConfig - return if (DataStore.serverConfig.isBlank()) { + val config = + (if (useConfigStore) DataStore.configurationStore.getString(configKey) else DataStore.serverConfig) + ?: "" + return if (config.isBlank()) { return app.resources.getString(androidx.preference.R.string.not_set) } else { app.resources.getString(R.string.lines, config.split('\n').size) diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt b/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt index 0b4509528..14f2240fd 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt @@ -1,5 +1,6 @@ package moe.matsuri.nb4a.utils +import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import androidx.appcompat.content.res.AppCompatResources @@ -9,6 +10,10 @@ import java.io.File // SagerNet Class +const val KB = 1024L +const val MB = KB * 1024 +const val GB = MB * 1024 + fun SagerNet.cleanWebview() { var pathToClean = "app_webview" if (isBgProcess) pathToClean += "_$process" @@ -34,6 +39,7 @@ fun File.recreate(dir: Boolean) { // Context utils +@SuppressLint("DiscouragedApi") fun Context.getDrawableByName(name: String?): Drawable? { val resourceId: Int = resources.getIdentifier(name, "drawable", packageName) return AppCompatResources.getDrawable(this, resourceId) @@ -42,12 +48,17 @@ fun Context.getDrawableByName(name: String?): Drawable? { // Traffic display fun Long.toBytesString(): String { + val size = this.toDouble() return when { - this > 1024 * 1024 * 1024 -> String.format( - "%.2f GiB", (this.toDouble() / 1024 / 1024 / 1024) - ) - this > 1024 * 1024 -> String.format("%.2f MiB", (this.toDouble() / 1024 / 1024)) - this > 1024 -> String.format("%.2f KiB", (this.toDouble() / 1024)) + this >= GB -> String.format("%.2f GiB", size / GB) + this >= MB -> String.format("%.2f MiB", size / MB) + this >= KB -> String.format("%.2f KiB", size / KB) else -> "$this Bytes" } } + +// List + +fun String.listByLineOrComma(): List { + return this.split(",","\n").map { it.trim() }.filter { it.isNotEmpty() } +} diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt index 66cdfbbc0..c5ae09abb 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt @@ -3,7 +3,10 @@ package moe.matsuri.nb4a.utils import android.annotation.SuppressLint import android.content.Context import android.util.Base64 +import libcore.StringBox import java.io.ByteArrayOutputStream +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.* import java.util.zip.Deflater @@ -21,7 +24,7 @@ object Util { */ fun getSubString(text: String, left: String?, right: String?): String { var zLen: Int - if (left == null || left.isEmpty()) { + if (left.isNullOrEmpty()) { zLen = 0 } else { zLen = text.indexOf(left) @@ -32,7 +35,7 @@ object Util { } } var yLen = if (right == null) -1 else text.indexOf(right, zLen) - if (yLen < 0 || right == null || right.isEmpty()) { + if (yLen < 0 || right.isNullOrEmpty()) { yLen = text.length } return text.substring(zLen, yLen) @@ -72,7 +75,7 @@ object Util { for (flag in flags) { try { ret = Base64.decode(str, flag) - } catch (e: Exception) { + } catch (_: Exception) { } if (ret != null) return ret } @@ -113,6 +116,48 @@ object Util { } } + fun map2StringMap(m: Map<*, *>): MutableMap { + val o = mutableMapOf() + m.forEach { + if (it.key is String) { + o[it.key as String] = it.value as Any + } + } + return o + } + + fun mergeMap(dst: MutableMap, src: Map): MutableMap { + src.forEach { (k, v) -> + if (v is Map<*, *> && dst[k] is Map<*, *>) { + val currentMap = (dst[k] as Map<*, *>).toMutableMap() + dst[k] = mergeMap(map2StringMap(currentMap), map2StringMap(v)) + } else if (v is List<*>) { + if (k.startsWith("+")) { // prepend + val dstKey = k.removePrefix("+") + var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf() + currentList = (v + currentList).toMutableList() + dst[dstKey] = currentList + } else if (k.endsWith("+")) { // append + val dstKey = k.removeSuffix("+") + var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf() + currentList = (currentList + v).toMutableList() + dst[dstKey] = currentList + } else { + dst[k] = v + } + } else { + dst[k] = v + } + } + return dst + } + + fun mergeJSON(dst: MutableMap, j: String) { + if (j.isBlank()) return + val src = JavaUtil.gson.fromJson(j, dst.javaClass) + mergeMap(dst, src) + } + // Format Time @SuppressLint("SimpleDateFormat") @@ -139,4 +184,17 @@ object Util { } } + fun getStringBox(b: StringBox?): String { + if (b != null && b.value != null) { + return b.value + } + return "" + } + + fun decodeFilename(headerValue: String): String { + val regex = Regex("filename\\*=[^']*''(.+)") + val match = regex.find(headerValue) + val encoded = match?.groupValues?.get(1) ?: "" + return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()) + } } diff --git a/app/src/main/res/layout/layout_about.xml b/app/src/main/res/layout/layout_about.xml index 2363c50f1..7e677577d 100644 --- a/app/src/main/res/layout/layout_about.xml +++ b/app/src/main/res/layout/layout_about.xml @@ -57,8 +57,7 @@ + android:layout_height="match_parent" /> + android:layout_height="wrap_content" + android:background="?attr/colorPrimary"> - - - - - - - - - - - - - - -