diff --git a/README.md b/README.md index 64fe9c8..a747d08 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,8 @@ Rootless 开箱即用的 Project Sekai 用户数据抓包工具,支持上传 - Suite: 登录步骤后,进入游戏的加载阶段(下载资源之前) - 烤森: 进入烤森 -- 烤森生日双叶: 我不知道 \ No newline at end of file +- 烤森生日双叶: 我不知道 + +## 上游代理 + +你可能会需要上游代理,有时它是一个运行在本地的应用,此时为了防止冲突,需要将对应的应用加入绕过列表。 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f646972..79bb4e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,8 +18,8 @@ android { applicationId = "net.bluemangoo.indpjsk" minSdk = appMinSdk targetSdk = 37 - versionCode = 1 - versionName = "1.1" + versionCode = 3 + versionName = "1.2" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a39cdc3..5aa4a59 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,12 +1,13 @@ - + + tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission"/> - + + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths"/> diff --git a/app/src/main/java/net/bluemangoo/indpjsk/App.kt b/app/src/main/java/net/bluemangoo/indpjsk/App.kt new file mode 100644 index 0000000..d8ad952 --- /dev/null +++ b/app/src/main/java/net/bluemangoo/indpjsk/App.kt @@ -0,0 +1,16 @@ +package net.bluemangoo.indpjsk + +import android.app.Application +import android.content.Context + +class App : Application() { + companion object { + lateinit var appContext: Context + private set + } + + override fun onCreate() { + super.onCreate() + appContext = applicationContext + } +} \ No newline at end of file diff --git a/app/src/main/java/net/bluemangoo/indpjsk/data/CaptureProfileStep.kt b/app/src/main/java/net/bluemangoo/indpjsk/data/CaptureProfileStep.kt index f9d2292..2157709 100644 --- a/app/src/main/java/net/bluemangoo/indpjsk/data/CaptureProfileStep.kt +++ b/app/src/main/java/net/bluemangoo/indpjsk/data/CaptureProfileStep.kt @@ -11,24 +11,15 @@ data class CaptureProfile( data class CaptureProfileStep( val id: String, val name: String, - /** 该步骤可能命中的上游域名;TW/KR 的 cdn / https 两个域名都会出现 */ val hosts: List, - /** 匹配 path + query 的正则(Rust 侧 Regex::is_match) */ val path: String, val fileName: String, - /** - * 该步骤抓成功后异步执行(不阻塞下一步的监听)。 - * 没有填上传码时会走这里;参数是本次抓包的结果。 - */ - val postAction: suspend (CaptureResult) -> Unit = {}, + var postAction: suspend (CaptureResult) -> Unit = {}, ) -/** 一步抓包完成后的结果,交给 postAction 用 */ data class CaptureResult( val profile: CaptureProfile, val step: CaptureProfileStep, - /** 命中的原始完整 URL(https://host/path?query,MySekai 的 flag 已改写为 True) */ val url: String, - /** 抓到的响应体文件 */ val file: File, ) diff --git a/app/src/main/java/net/bluemangoo/indpjsk/data/PredefinedProfiles.kt b/app/src/main/java/net/bluemangoo/indpjsk/data/PredefinedProfiles.kt index 1e50983..5348de0 100644 --- a/app/src/main/java/net/bluemangoo/indpjsk/data/PredefinedProfiles.kt +++ b/app/src/main/java/net/bluemangoo/indpjsk/data/PredefinedProfiles.kt @@ -1,16 +1,8 @@ package net.bluemangoo.indpjsk.data -object PredefinedProfiles { +import net.bluemangoo.indpjsk.upload.Uploader - /** - * 两个数据接口的正则(Host 已去掉,Rust 侧是拿 path + query 去匹配的): - * - * 1. Suite(个人信息 / 卡牌 / 仓库主数据) - * Path 模板: /api/suite/user/{uid} - * - * 2. MySekai(烤森房间 / 地图全量数据) - * Path 模板: /api/user/{uid}/mysekai?isForceAllReloadOnlyMysekai=True - */ +object PredefinedProfiles { private const val SUITE_PATH = "/api/suite/user/(\\d+)(\\?isLogin=true)?\$" private const val MYSEKAI_PATH = "/api/user/(\\d+)/mysekai\\?isForceAllReloadOnlyMysekai=True" @@ -20,7 +12,10 @@ object PredefinedProfiles { name = "Suite", hosts = hosts, path = SUITE_PATH, - fileName = "suite" + fileName = "suite", + postAction = { result -> + Uploader.tryUpload(result) + } ) private fun mysekaiStep(hosts: List) = CaptureProfileStep( @@ -28,7 +23,10 @@ object PredefinedProfiles { name = "烤森", hosts = hosts, path = MYSEKAI_PATH, - fileName = "mysekai" + fileName = "mysekai", + postAction = { result -> + Uploader.tryUpload(result) + } ) private fun mysekaiBirthday(hosts: List) = CaptureProfileStep( @@ -36,7 +34,10 @@ object PredefinedProfiles { name = "烤森生日双叶", hosts = hosts, path = "/api/user/(\\d+)/mysekai/birthday-party/(\\d+)/delivery", - fileName = "mysekai_birthday" + fileName = "mysekai_birthday", + postAction = { result -> + Uploader.tryUpload(result) + } ) private val CN_HOSTS = listOf("mkcn-prod-public-60001-1.dailygn.com") @@ -51,6 +52,15 @@ object PredefinedProfiles { "mkkorea-obt-prod01-https.bytedgame.com" ) + fun rewriteTwUrl(result: CaptureResult): CaptureResult { + val originalUrl = result.url + val rewrittenUrl = originalUrl.replace( + "mk-zian-obt-https.bytedgame.com", + "mk-zian-obt-cdn.bytedgame.com" + ) + return result.copy(url = rewrittenUrl) + } + val all: List = listOf( // 国服不支持烤森 CaptureProfile( @@ -71,7 +81,22 @@ object PredefinedProfiles { CaptureProfile( id = "tw", name = "台服", - steps = listOf(suiteStep(TW_HOSTS), mysekaiStep(TW_HOSTS), mysekaiBirthday(TW_HOSTS)) + steps = listOf(suiteStep(TW_HOSTS).apply { + postAction = { result -> + val rewrittenResult = rewriteTwUrl(result) + Uploader.tryUpload(rewrittenResult) + } + }, mysekaiStep(TW_HOSTS).apply { + postAction = { result -> + val rewrittenResult = rewriteTwUrl(result) + Uploader.tryUpload(rewrittenResult) + } + }, mysekaiBirthday(TW_HOSTS).apply { + postAction = { result -> + val rewrittenResult = rewriteTwUrl(result) + Uploader.tryUpload(rewrittenResult) + } + }) ), CaptureProfile( id = "kr", diff --git a/app/src/main/java/net/bluemangoo/indpjsk/upload/Uploader.kt b/app/src/main/java/net/bluemangoo/indpjsk/upload/Uploader.kt index 1c4416d..0677586 100644 --- a/app/src/main/java/net/bluemangoo/indpjsk/upload/Uploader.kt +++ b/app/src/main/java/net/bluemangoo/indpjsk/upload/Uploader.kt @@ -2,6 +2,9 @@ package net.bluemangoo.indpjsk.upload import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import net.bluemangoo.indpjsk.App +import net.bluemangoo.indpjsk.data.CaptureResult +import net.bluemangoo.indpjsk.data.SettingsStore import net.bluemangoo.indpjsk.vpn.VpnLog import java.io.File import java.io.IOException @@ -55,6 +58,15 @@ object Uploader { } } + suspend fun tryUpload(result: CaptureResult) { + val uploadCode = SettingsStore.loadUploadCode(App.appContext).trim() + if (uploadCode.isNotEmpty()) { + VpnLog.info("${result.profile.name}${result.step.name} 开始上传") + upload(uploadCode, result.url, result.file) + VpnLog.success("${result.profile.name}${result.step.name} 上传完成") + } + } + private fun postChunk( uploadCode: String, uploadId: String, diff --git a/app/src/main/java/net/bluemangoo/indpjsk/vpn/CaptureVpnService.kt b/app/src/main/java/net/bluemangoo/indpjsk/vpn/CaptureVpnService.kt index 461ba1c..f1e286a 100644 --- a/app/src/main/java/net/bluemangoo/indpjsk/vpn/CaptureVpnService.kt +++ b/app/src/main/java/net/bluemangoo/indpjsk/vpn/CaptureVpnService.kt @@ -4,19 +4,9 @@ import android.annotation.SuppressLint import android.content.Intent import android.net.VpnService import android.os.ParcelFileDescriptor -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import net.bluemangoo.indpjsk.data.CaptureProfile -import net.bluemangoo.indpjsk.data.CaptureProfileStep -import net.bluemangoo.indpjsk.data.CaptureResult -import net.bluemangoo.indpjsk.data.PredefinedProfiles -import net.bluemangoo.indpjsk.data.SettingsStore -import net.bluemangoo.indpjsk.data.StepSelectionStore +import kotlinx.coroutines.* +import net.bluemangoo.indpjsk.data.* import net.bluemangoo.indpjsk.state.CaptureStateHolder -import net.bluemangoo.indpjsk.upload.Uploader import java.io.File @SuppressLint("VpnServicePolicy") @@ -98,11 +88,6 @@ class CaptureVpnService : VpnService() { return } - val uploadCode = SettingsStore.loadUploadCode(this).trim() - if (uploadCode.isNotEmpty() && uploadCode.length != 32) { - VpnLog.warn("上传码长度不是 32 位(当前 ${uploadCode.length} 位),上传大概率会失败") - } - val proxy = SettingsStore.loadProxy(this).trim() val bypassPackages = SettingsStore.loadBypassPackages(this) if (proxy.isNotEmpty()) VpnLog.info("上游代理: $proxy") @@ -228,6 +213,7 @@ class CaptureVpnService : VpnService() { VpnLog.error(errorMsg) break } + VpnLog.info("$label 抓到目标请求: $capturedUrl") if (!target.exists() || target.length() == 0L) { errorMsg = "$label 返回了 URL 但文件为空" VpnLog.error(errorMsg) @@ -241,13 +227,7 @@ class CaptureVpnService : VpnService() { val result = CaptureResult(profile, step, capturedUrl, target) postScope.launch { try { - if (uploadCode.isNotEmpty()) { - VpnLog.info("$label 开始上传") - Uploader.upload(uploadCode, capturedUrl, target) - VpnLog.success("${profile.name}${step.name} 上传完成") - } else { - step.postAction(result) - } + step.postAction(result) } catch (c: CancellationException) { VpnLog.warn("$label 后处理被取消: ${c.message}") throw c diff --git a/core_vpn/src/mitm.rs b/core_vpn/src/mitm.rs index 5f9bbb7..f6dd3ed 100644 --- a/core_vpn/src/mitm.rs +++ b/core_vpn/src/mitm.rs @@ -191,7 +191,6 @@ where break; } _ = cancel.cancelled(), if !graceful => { - info!("teardown: graceful shutdown of downstream connection"); conn.as_mut().graceful_shutdown(); graceful = true; } @@ -352,12 +351,10 @@ fn build_url(host: &str, uri: &Uri) -> String { format!("https://{host}{pq}") } -/// 把 MySekai 的增量刷新标志强制改成 True(和模块的 URL Rewrite 一致), -/// 其它 query 参数原样保留。没有这个参数、或已经是 True 时返回 None。 fn force_full_mysekai(uri: &Uri) -> Option { const KEY: &str = "isForceAllReloadOnlyMysekai"; let mut query = qstring::QString::from(uri.query()?); - if !query.has(KEY) || query.get(KEY) == Some("True") { + if query.get(KEY) != Some("False") { return None; } query.set(KEY, "True");