1 Commits
Author SHA1 Message Date
Bluemangoo 0851713876 haruki需要替换台服的host 2026-10-26 23:27:46 +08:00
9 changed files with 90 additions and 64 deletions
+5 -1
View File
@@ -15,4 +15,8 @@ Rootless 开箱即用的 Project Sekai 用户数据抓包工具,支持上传
- Suite: 登录步骤后,进入游戏的加载阶段(下载资源之前)
- 烤森: 进入烤森
- 烤森生日双叶: 我不知道
- 烤森生日双叶: 我不知道
## 上游代理
你可能会需要上游代理,有时它是一个运行在本地的应用,此时为了防止冲突,需要将对应的应用加入绕过列表。
+2 -2
View File
@@ -18,8 +18,8 @@ android {
applicationId = "net.bluemangoo.indpjsk"
minSdk = appMinSdk
targetSdk = 37
versionCode = 1
versionName = "1.1"
versionCode = 3
versionName = "1.2"
}
+10 -9
View File
@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission"/>
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -31,18 +32,18 @@
android:exported="true"
android:permission="android.permission.BIND_VPN_SERVICE" tools:ignore="VpnServicePolicy">
<intent-filter>
<action android:name="android.net.VpnService" />
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
</application>
@@ -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
}
}
@@ -11,24 +11,15 @@ data class CaptureProfile(
data class CaptureProfileStep(
val id: String,
val name: String,
/** 该步骤可能命中的上游域名;TW/KR 的 cdn / https 两个域名都会出现 */
val hosts: List<String>,
/** 匹配 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,
)
@@ -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<String>) = 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<String>) = 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<CaptureProfile> = 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",
@@ -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,
@@ -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
+1 -4
View File
@@ -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<Uri> {
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");