Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0851713876
|
@@ -16,3 +16,7 @@ Rootless 开箱即用的 Project Sekai 用户数据抓包工具,支持上传
|
||||
- Suite: 登录步骤后,进入游戏的加载阶段(下载资源之前)
|
||||
- 烤森: 进入烤森
|
||||
- 烤森生日双叶: 我不知道
|
||||
|
||||
## 上游代理
|
||||
|
||||
你可能会需要上游代理,有时它是一个运行在本地的应用,此时为了防止冲突,需要将对应的应用加入绕过列表。
|
||||
|
||||
@@ -18,8 +18,8 @@ android {
|
||||
applicationId = "net.bluemangoo.indpjsk"
|
||||
minSdk = appMinSdk
|
||||
targetSdk = 37
|
||||
versionCode = 1
|
||||
versionName = "1.1"
|
||||
versionCode = 3
|
||||
versionName = "1.2"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user