Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0851713876
|
@@ -16,3 +16,7 @@ Rootless 开箱即用的 Project Sekai 用户数据抓包工具,支持上传
|
|||||||
- Suite: 登录步骤后,进入游戏的加载阶段(下载资源之前)
|
- Suite: 登录步骤后,进入游戏的加载阶段(下载资源之前)
|
||||||
- 烤森: 进入烤森
|
- 烤森: 进入烤森
|
||||||
- 烤森生日双叶: 我不知道
|
- 烤森生日双叶: 我不知道
|
||||||
|
|
||||||
|
## 上游代理
|
||||||
|
|
||||||
|
你可能会需要上游代理,有时它是一个运行在本地的应用,此时为了防止冲突,需要将对应的应用加入绕过列表。
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId = "net.bluemangoo.indpjsk"
|
applicationId = "net.bluemangoo.indpjsk"
|
||||||
minSdk = appMinSdk
|
minSdk = appMinSdk
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 1
|
versionCode = 3
|
||||||
versionName = "1.1"
|
versionName = "1.2"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE" tools:ignore="VpnServicePolicy">
|
android:permission="android.permission.BIND_VPN_SERVICE" tools:ignore="VpnServicePolicy">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths"/>
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</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(
|
data class CaptureProfileStep(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
/** 该步骤可能命中的上游域名;TW/KR 的 cdn / https 两个域名都会出现 */
|
|
||||||
val hosts: List<String>,
|
val hosts: List<String>,
|
||||||
/** 匹配 path + query 的正则(Rust 侧 Regex::is_match) */
|
|
||||||
val path: String,
|
val path: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
/**
|
var postAction: suspend (CaptureResult) -> Unit = {},
|
||||||
* 该步骤抓成功后异步执行(不阻塞下一步的监听)。
|
|
||||||
* 没有填上传码时会走这里;参数是本次抓包的结果。
|
|
||||||
*/
|
|
||||||
val postAction: suspend (CaptureResult) -> Unit = {},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 一步抓包完成后的结果,交给 postAction 用 */
|
|
||||||
data class CaptureResult(
|
data class CaptureResult(
|
||||||
val profile: CaptureProfile,
|
val profile: CaptureProfile,
|
||||||
val step: CaptureProfileStep,
|
val step: CaptureProfileStep,
|
||||||
/** 命中的原始完整 URL(https://host/path?query,MySekai 的 flag 已改写为 True) */
|
|
||||||
val url: String,
|
val url: String,
|
||||||
/** 抓到的响应体文件 */
|
|
||||||
val file: File,
|
val file: File,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
package net.bluemangoo.indpjsk.data
|
package net.bluemangoo.indpjsk.data
|
||||||
|
|
||||||
object PredefinedProfiles {
|
import net.bluemangoo.indpjsk.upload.Uploader
|
||||||
|
|
||||||
/**
|
object PredefinedProfiles {
|
||||||
* 两个数据接口的正则(Host 已去掉,Rust 侧是拿 path + query 去匹配的):
|
|
||||||
*
|
|
||||||
* 1. Suite(个人信息 / 卡牌 / 仓库主数据)
|
|
||||||
* Path 模板: /api/suite/user/{uid}
|
|
||||||
*
|
|
||||||
* 2. MySekai(烤森房间 / 地图全量数据)
|
|
||||||
* Path 模板: /api/user/{uid}/mysekai?isForceAllReloadOnlyMysekai=True
|
|
||||||
*/
|
|
||||||
private const val SUITE_PATH = "/api/suite/user/(\\d+)(\\?isLogin=true)?\$"
|
private const val SUITE_PATH = "/api/suite/user/(\\d+)(\\?isLogin=true)?\$"
|
||||||
private const val MYSEKAI_PATH =
|
private const val MYSEKAI_PATH =
|
||||||
"/api/user/(\\d+)/mysekai\\?isForceAllReloadOnlyMysekai=True"
|
"/api/user/(\\d+)/mysekai\\?isForceAllReloadOnlyMysekai=True"
|
||||||
@@ -20,7 +12,10 @@ object PredefinedProfiles {
|
|||||||
name = "Suite",
|
name = "Suite",
|
||||||
hosts = hosts,
|
hosts = hosts,
|
||||||
path = SUITE_PATH,
|
path = SUITE_PATH,
|
||||||
fileName = "suite"
|
fileName = "suite",
|
||||||
|
postAction = { result ->
|
||||||
|
Uploader.tryUpload(result)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun mysekaiStep(hosts: List<String>) = CaptureProfileStep(
|
private fun mysekaiStep(hosts: List<String>) = CaptureProfileStep(
|
||||||
@@ -28,7 +23,10 @@ object PredefinedProfiles {
|
|||||||
name = "烤森",
|
name = "烤森",
|
||||||
hosts = hosts,
|
hosts = hosts,
|
||||||
path = MYSEKAI_PATH,
|
path = MYSEKAI_PATH,
|
||||||
fileName = "mysekai"
|
fileName = "mysekai",
|
||||||
|
postAction = { result ->
|
||||||
|
Uploader.tryUpload(result)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun mysekaiBirthday(hosts: List<String>) = CaptureProfileStep(
|
private fun mysekaiBirthday(hosts: List<String>) = CaptureProfileStep(
|
||||||
@@ -36,7 +34,10 @@ object PredefinedProfiles {
|
|||||||
name = "烤森生日双叶",
|
name = "烤森生日双叶",
|
||||||
hosts = hosts,
|
hosts = hosts,
|
||||||
path = "/api/user/(\\d+)/mysekai/birthday-party/(\\d+)/delivery",
|
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")
|
private val CN_HOSTS = listOf("mkcn-prod-public-60001-1.dailygn.com")
|
||||||
@@ -51,6 +52,15 @@ object PredefinedProfiles {
|
|||||||
"mkkorea-obt-prod01-https.bytedgame.com"
|
"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(
|
val all: List<CaptureProfile> = listOf(
|
||||||
// 国服不支持烤森
|
// 国服不支持烤森
|
||||||
CaptureProfile(
|
CaptureProfile(
|
||||||
@@ -71,7 +81,22 @@ object PredefinedProfiles {
|
|||||||
CaptureProfile(
|
CaptureProfile(
|
||||||
id = "tw",
|
id = "tw",
|
||||||
name = "台服",
|
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(
|
CaptureProfile(
|
||||||
id = "kr",
|
id = "kr",
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package net.bluemangoo.indpjsk.upload
|
|||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
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 net.bluemangoo.indpjsk.vpn.VpnLog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
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(
|
private fun postChunk(
|
||||||
uploadCode: String,
|
uploadCode: String,
|
||||||
uploadId: String,
|
uploadId: String,
|
||||||
|
|||||||
@@ -4,19 +4,9 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import net.bluemangoo.indpjsk.data.*
|
||||||
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 net.bluemangoo.indpjsk.state.CaptureStateHolder
|
import net.bluemangoo.indpjsk.state.CaptureStateHolder
|
||||||
import net.bluemangoo.indpjsk.upload.Uploader
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@SuppressLint("VpnServicePolicy")
|
@SuppressLint("VpnServicePolicy")
|
||||||
@@ -98,11 +88,6 @@ class CaptureVpnService : VpnService() {
|
|||||||
return
|
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 proxy = SettingsStore.loadProxy(this).trim()
|
||||||
val bypassPackages = SettingsStore.loadBypassPackages(this)
|
val bypassPackages = SettingsStore.loadBypassPackages(this)
|
||||||
if (proxy.isNotEmpty()) VpnLog.info("上游代理: $proxy")
|
if (proxy.isNotEmpty()) VpnLog.info("上游代理: $proxy")
|
||||||
@@ -228,6 +213,7 @@ class CaptureVpnService : VpnService() {
|
|||||||
VpnLog.error(errorMsg)
|
VpnLog.error(errorMsg)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
VpnLog.info("$label 抓到目标请求: $capturedUrl")
|
||||||
if (!target.exists() || target.length() == 0L) {
|
if (!target.exists() || target.length() == 0L) {
|
||||||
errorMsg = "$label 返回了 URL 但文件为空"
|
errorMsg = "$label 返回了 URL 但文件为空"
|
||||||
VpnLog.error(errorMsg)
|
VpnLog.error(errorMsg)
|
||||||
@@ -241,13 +227,7 @@ class CaptureVpnService : VpnService() {
|
|||||||
val result = CaptureResult(profile, step, capturedUrl, target)
|
val result = CaptureResult(profile, step, capturedUrl, target)
|
||||||
postScope.launch {
|
postScope.launch {
|
||||||
try {
|
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) {
|
} catch (c: CancellationException) {
|
||||||
VpnLog.warn("$label 后处理被取消: ${c.message}")
|
VpnLog.warn("$label 后处理被取消: ${c.message}")
|
||||||
throw c
|
throw c
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ where
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ = cancel.cancelled(), if !graceful => {
|
_ = cancel.cancelled(), if !graceful => {
|
||||||
info!("teardown: graceful shutdown of downstream connection");
|
|
||||||
conn.as_mut().graceful_shutdown();
|
conn.as_mut().graceful_shutdown();
|
||||||
graceful = true;
|
graceful = true;
|
||||||
}
|
}
|
||||||
@@ -352,12 +351,10 @@ fn build_url(host: &str, uri: &Uri) -> String {
|
|||||||
format!("https://{host}{pq}")
|
format!("https://{host}{pq}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把 MySekai 的增量刷新标志强制改成 True(和模块的 URL Rewrite 一致),
|
|
||||||
/// 其它 query 参数原样保留。没有这个参数、或已经是 True 时返回 None。
|
|
||||||
fn force_full_mysekai(uri: &Uri) -> Option<Uri> {
|
fn force_full_mysekai(uri: &Uri) -> Option<Uri> {
|
||||||
const KEY: &str = "isForceAllReloadOnlyMysekai";
|
const KEY: &str = "isForceAllReloadOnlyMysekai";
|
||||||
let mut query = qstring::QString::from(uri.query()?);
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
query.set(KEY, "True");
|
query.set(KEY, "True");
|
||||||
|
|||||||
Reference in New Issue
Block a user