fix(i18n): 完善国际化并迁移 WLOC 脚本

This commit is contained in:
xweiba
2026-09-11 02:39:00 +08:00
parent 908ac8f734
commit 05949703af
42 changed files with 2006 additions and 160 deletions
+32 -18
View File
@@ -95,9 +95,10 @@ struct BugReportView: View {
do { do {
let response = try await thirdPartyProxy.query() let response = try await thirdPartyProxy.query()
let active = response.success && response.latitude != nil && response.longitude != nil let active = response.success && response.latitude != nil && response.longitude != nil
testLog = "第三方代理测试模式:模块连接成功;已保存坐标=\(active ? "是" : "否")" let savedCoordinate = active ? String(localized: "是") : String(localized: "否")
testLog = String(localized: "第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
} catch { } catch {
testLog = "第三方代理测试模式:模块连接失败;\(error.localizedDescription)" testLog = String(localized: "第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
} }
} else { } else {
_ = await setup.runVerificationTest() _ = await setup.runVerificationTest()
@@ -113,22 +114,11 @@ struct BugReportView: View {
let systemVersion = UIDevice.current.systemVersion let systemVersion = UIDevice.current.systemVersion
// 拼接报告 // 拼接报告
let report = """ let report = bugReport(
### 环境信息 appVersion: appVersion,
App 版本: \(appVersion) systemVersion: systemVersion,
系统版本: iOS \(systemVersion) testLog: testLog
运行模式: \(runtimeMode.mode.displayName) )
第三方客户端: \(runtimeMode.mode == .thirdParty ? thirdPartyClient.selectedClient.name : "不适用")
可复现环境: \(isReproducible ? "是" : "否")
### 问题描述
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
### 诊断日志
```
\(testLog.isEmpty ? "(无诊断数据)" : testLog)
```
"""
// 复制到剪切板 // 复制到剪切板
UIPasteboard.general.string = report UIPasteboard.general.string = report
@@ -138,4 +128,28 @@ struct BugReportView: View {
showCopiedAlert = true showCopiedAlert = true
} }
} }
private func bugReport(appVersion: String, systemVersion: String, testLog: String) -> String {
let client = runtimeMode.mode == .thirdParty
? thirdPartyClient.selectedClient.name
: String(localized: "不适用")
let reproducible = isReproducible ? String(localized: "是") : String(localized: "否")
let diagnostics = testLog.isEmpty ? String(localized: "(无诊断数据)") : testLog
return """
### \(String(localized: "环境信息"))
\(String(localized: "App 版本")): \(appVersion)
\(String(localized: "系统版本")): iOS \(systemVersion)
\(String(localized: "运行模式")): \(runtimeMode.mode.displayName)
\(String(localized: "第三方客户端")): \(client)
\(String(localized: "可复现环境")): \(reproducible)
### \(String(localized: "问题描述"))
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
### \(String(localized: "诊断日志"))
```
\(diagnostics)
```
"""
}
} }
+24 -17
View File
@@ -23,7 +23,10 @@ struct RuntimeLogsView: View {
private var filteredEntries: [RuntimeLogEntry] { private var filteredEntries: [RuntimeLogEntry] {
let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines) let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines)
guard !q.isEmpty else { return entries } guard !q.isEmpty else { return entries }
return entries.filter { $0.message.localizedCaseInsensitiveContains(q) } return entries.filter {
$0.localizedMessage.localizedCaseInsensitiveContains(q)
|| $0.localizedCategory.localizedCaseInsensitiveContains(q)
}
} }
var body: some View { var body: some View {
@@ -201,7 +204,7 @@ struct RuntimeLogsView: View {
.foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18) .foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {
Text("\(entry.source) \(entry.category)").font(.caption.weight(.semibold)) Text("\(entry.source) \(entry.localizedCategory)").font(.caption.weight(.semibold))
Spacer() Spacer()
Button { Button {
UIPasteboard.general.string = entry.renderedText UIPasteboard.general.string = entry.renderedText
@@ -225,9 +228,9 @@ struct RuntimeLogsView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
Text(entry.message).font(.caption.monospaced()).textSelection(.enabled) Text(entry.localizedMessage).font(.caption.monospaced()).textSelection(.enabled)
if !entry.details.isEmpty { if !entry.details.isEmpty {
Text(entry.details.sorted(by: { $0.key < $1.key }).map { "\($0.key): \($0.value)" }.joined(separator: "\n")) Text(entry.localizedDetailsText)
.font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled) .font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled)
} }
} }
@@ -246,22 +249,26 @@ struct RuntimeLogsView: View {
let active = response.success && response.latitude != nil && response.longitude != nil let active = response.success && response.latitude != nil && response.longitude != nil
testSucceeded = true testSucceeded = true
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标") testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
testMessage = """ testMessage = thirdPartyTestLog(active: active)
======== 第三方代理连接检测 ========
模式: 测试模式
请求: wloc-settings/save?action=query
拦截响应: 有效 JSON
已保存坐标: \(active ? "是" : "否")
"""
} catch { } catch {
testSucceeded = false testSucceeded = false
testResult = String(localized: "第三方模块连接失败") testResult = String(localized: "第三方模块连接失败")
testMessage = """ testMessage = thirdPartyTestLog(error: error)
======== 第三方代理连接检测 ========
模式: 测试模式
请求: wloc-settings/save?action=query
结果: \(error.localizedDescription)
"""
} }
} }
private func thirdPartyTestLog(active: Bool? = nil, error: Error? = nil) -> String {
var lines = [
String(localized: "======== 第三方代理连接检测 ========"),
String(localized: "模式: 测试模式"),
String(localized: "请求: wloc-settings/save?action=query")
]
if let active {
lines.append(String(localized: "拦截响应: 有效 JSON"))
lines.append(String(localized: "已保存坐标: \(active ? String(localized: "是") : String(localized: "否"))"))
} else if let error {
lines.append(String(localized: "结果: \(error.localizedDescription)"))
}
return lines.joined(separator: "\n")
}
} }
+34 -32
View File
@@ -220,15 +220,15 @@ struct FirstSetupView: View {
return thirdPartyTestFailure.message return thirdPartyTestFailure.message
} }
guard !setup.message.isEmpty else { return nil } guard !setup.message.isEmpty else { return nil }
return """ return [
======== 第三方代理运行检测 ======== String(localized: "======== 第三方代理运行检测 ========"),
当前客户端:\(thirdPartyClient.selectedClient.name) String(localized: "当前客户端:\(thirdPartyClient.selectedClient.name)"),
触发来源:地图或设置中的第三方代理操作 String(localized: "触发来源:地图或设置中的第三方代理操作"),
请求动作:WLOC 配置接口 String(localized: "请求动作:WLOC 配置接口"),
检测结果:失败 String(localized: "检测结果:失败"),
错误详情:\(setup.message) String(localized: "错误详情:\(setup.message)"),
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。 String(localized: "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
""" ].joined(separator: "\n")
} }
private var modeStep: some View { private var modeStep: some View {
@@ -468,10 +468,10 @@ struct FirstSetupView: View {
Text("返回格式") Text("返回格式")
.font(.subheadline.bold()) .font(.subheadline.bold())
Text(""" Text(String(localized: """
成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25} 成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25}
失败:{"success":false,"error":"错误说明"} 失败:{"success":false,"error":"错误说明"}
""") """))
.font(.caption.monospaced()) .font(.caption.monospaced())
.textSelection(.enabled) .textSelection(.enabled)
@@ -641,7 +641,7 @@ struct FirstSetupView: View {
private func setupScreenshot( private func setupScreenshot(
assetName: String, assetName: String,
title: String, title: String,
caption: LocalizedStringKey caption: String
) -> some View { ) -> some View {
if let image = UIImage(named: assetName) { if let image = UIImage(named: assetName) {
Button { Button {
@@ -657,7 +657,7 @@ struct FirstSetupView: View {
.clipShape(RoundedRectangle(cornerRadius: 6)) .clipShape(RoundedRectangle(cornerRadius: 6))
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: "arrow.up.left.and.arrow.down.right") Image(systemName: "arrow.up.left.and.arrow.down.right")
Text(caption) Text(LocalizedStringKey(caption))
} }
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -672,7 +672,11 @@ struct FirstSetupView: View {
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(LocalizedStringKey(title)) .accessibilityLabel(
Text(LocalizedStringKey(title))
+ Text(": ")
+ Text(LocalizedStringKey(caption))
)
.accessibilityHint("轻点查看大图") .accessibilityHint("轻点查看大图")
} }
} }
@@ -872,21 +876,19 @@ struct FirstSetupView: View {
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error) "处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
] ]
) )
thirdPartyTestFailure = ThirdPartyConnectionTestFailure( thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
message: """ String(localized: "======== 第三方代理连接检测 ========"),
======== 第三方代理连接检测 ======== String(localized: "当前客户端:\(client.name)"),
当前客户端:\(client.name) String(localized: "配置接口:/wloc-settings/save"),
配置接口:/wloc-settings/save String(localized: "请求动作:WLOC query"),
请求动作:WLOC query String(localized: "检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
检查范围:模块拦截、MITM、证书、代理/VPN 连接 String(localized: "连接状态:\(connectionState)"),
连接状态:\(connectionState) String(localized: "检测结果:失败"),
检测结果:失败 String(localized: "耗时:\(elapsedMilliseconds) ms"),
耗时:\(elapsedMilliseconds) ms String(localized: "错误类型:\(errorType)"),
错误类型:\(errorType) String(localized: "错误详情:\(error.localizedDescription)"),
错误详情:\(error.localizedDescription) String(localized: "处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。")
处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。 ].joined(separator: "\n"))
"""
)
showsThirdPartyFailureLog = true showsThirdPartyFailureLog = true
} }
} }
@@ -895,11 +897,11 @@ struct FirstSetupView: View {
private var thirdPartyConnectionStateDescription: String { private var thirdPartyConnectionStateDescription: String {
switch thirdPartyProxy.connectionState { switch thirdPartyProxy.connectionState {
case .unknown: case .unknown:
return "未检测" return String(localized: "未检测")
case .connected(let active): case .connected(let active):
return active ? "已连接,有保存坐标" : "已连接,无保存坐标" return active ? String(localized: "已连接,有保存坐标") : String(localized: "已连接,无保存坐标")
case .failed(let message): case .failed(let message):
return "连接失败(\(message))" return String(localized: "连接失败(\(message))")
} }
} }
+5 -2
View File
@@ -766,7 +766,8 @@ struct MapHomeView: View {
private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) { private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) {
let count = tipPreferences.recordSuccessfulOperation(kind) let count = tipPreferences.recordSuccessfulOperation(kind)
let operationName = kind == .activation ? "开启" : "关闭" let operationName = kind == .activation ? "开启" : "关闭"
RuntimeLogger.info("APP", "提醒", "累计\(operationName)虚拟定位次数", details: [ RuntimeLogger.info("APP", "提醒", "累计虚拟定位操作次数", details: [
"操作": operationName,
"次数": String(count), "次数": String(count),
"运行模式": runtimeMode.mode.displayName, "运行模式": runtimeMode.mode.displayName,
"可显示不再提醒": String(tipPreferences.canSuppress(kind)) "可显示不再提醒": String(tipPreferences.canSuppress(kind))
@@ -1335,7 +1336,9 @@ struct MapHomeView: View {
context.showFailureAlert, context.showFailureAlert,
!Task.isCancelled, !Task.isCancelled,
mapState.selection.revision == context.intent.selectionRevision else { return } mapState.selection.revision == context.intent.selectionRevision else { return }
RuntimeLogger.info("APP", "地图", "定位失败 status=\(realtime.authorizationStatus.rawValue)") RuntimeLogger.info("APP", "地图", "定位失败", details: [
"授权状态rawValue": String(realtime.authorizationStatus.rawValue)
])
showLocationAlert = true showLocationAlert = true
return return
} }
+1 -1
View File
@@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable {
enum ProxyError: LocalizedError { enum ProxyError: LocalizedError {
case startFailed case startFailed
var errorDescription: String? { "Go proxy 启动失败" } var errorDescription: String? { String(localized: "Go proxy 启动失败") }
} }
+2 -2
View File
@@ -222,11 +222,11 @@ struct SettingsView: View {
Section("致谢") { Section("致谢") {
Button { Button {
if let url = URL(string: "https://github.com/Yu9191/wloc") { if let url = URL(string: "https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md") {
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
} label: { } label: {
Label("核心定位改写逻辑移植自 Yu9191/wloc", systemImage: "heart.fill") Label("定位改写脚本源自 Yu9191/wloc,现由本项目维护", systemImage: "heart.fill")
.foregroundStyle(.pink) .foregroundStyle(.pink)
} }
} }
+20 -20
View File
@@ -108,36 +108,36 @@ final class SetupCoordinator: ObservableObject {
testLog = "" testLog = ""
let log = { (msg: String) in self.testLog += msg + "\n" } let log = { (msg: String) in self.testLog += msg + "\n" }
log("======== 代理验证测试 ========") log(String(localized: "======== 代理验证测试 ========"))
log("App 版本: \(appVersion)") log(String(localized: "App 版本: \(appVersion)"))
log("系统版本: iOS \(UIDevice.current.systemVersion)") log(String(localized: "系统版本: iOS \(UIDevice.current.systemVersion)"))
log("") log("")
// Step A: Proxy running // Step A: Proxy running
log("[步骤 A] 检查代理是否运行…") log(String(localized: "[步骤 A] 检查代理是否运行…"))
log(" 端口: 127.0.0.1:8888") log(String(localized: " 端口: 127.0.0.1:8888"))
let stepAStart = Date() let stepAStart = Date()
if !proxy.isRunning { if !proxy.isRunning {
log(" ⚠ 代理未运行,尝试启动…") log(String(localized: " ⚠ 代理未运行,尝试启动…"))
do { try await proxy.start() } catch { do { try await proxy.start() } catch {
log(" ✗ 启动失败: \(error.localizedDescription)") log(String(localized: " ✗ 启动失败: \(error.localizedDescription)"))
return .proxyNotRunning return .proxyNotRunning
} }
log(" ✓ 代理启动成功") log(String(localized: " ✓ 代理启动成功"))
} else { } else {
log(" ✓ 代理已在运行中") log(String(localized: " ✓ 代理已在运行中"))
} }
collectProxyLogs(since: stepAStart, to: log) collectProxyLogs(since: stepAStart, to: log)
// Step B: Combined CA + WiFi proxy check (single request) // Step B: Combined CA + WiFi proxy check (single request)
log("") log("")
log("[步骤 B] 检测证书与 WiFi 代理…") log(String(localized: "[步骤 B] 检测证书与 WiFi 代理…"))
log(" 方式: 请求 baidu.com/paopao-verify-<token>") log(String(localized: " 方式: 请求 baidu.com/paopao-verify-<token>"))
log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过") log(String(localized: " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
let stepBStart = Date() let stepBStart = Date()
let verifyToken = CoreBridge.refreshVerifyToken() let verifyToken = CoreBridge.refreshVerifyToken()
guard !verifyToken.isEmpty else { guard !verifyToken.isEmpty else {
log(" ✗ 无法生成验证 token") log(String(localized: " ✗ 无法生成验证 token"))
return .certNotTrusted return .certNotTrusted
} }
do { do {
@@ -150,17 +150,17 @@ final class SetupCoordinator: ObservableObject {
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0 let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
let body = String(data: data, encoding: .utf8) ?? "" let body = String(data: data, encoding: .utf8) ?? ""
if body == verifyToken { if body == verifyToken {
log(" ✓ 证书已信任,WiFi 代理已配置") log(String(localized: " ✓ 证书已信任,WiFi 代理已配置"))
} else { } else {
log(" ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置") log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置"))
return .wifiProxyNotConfigured return .wifiProxyNotConfigured
} }
} catch { } catch {
let ns = error as NSError let ns = error as NSError
let msg = error.localizedDescription let msg = error.localizedDescription
log(" ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)") log(String(localized: " ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)"))
if isCertificateTrustError(nsError: ns, message: msg) { if isCertificateTrustError(nsError: ns, message: msg) {
log(" TLS/证书校验失败,CA 证书未信任") log(String(localized: " TLS/证书校验失败,CA 证书未信任"))
return .certNotTrusted return .certNotTrusted
} }
return .wifiProxyNotConfigured return .wifiProxyNotConfigured
@@ -168,7 +168,7 @@ final class SetupCoordinator: ObservableObject {
collectProxyLogs(since: stepBStart, to: log) collectProxyLogs(since: stepBStart, to: log)
log("") log("")
log("======== 环境检测通过 ✓ ========") log(String(localized: "======== 环境检测通过 ✓ ========"))
return .success return .success
} }
@@ -202,9 +202,9 @@ final class SetupCoordinator: ObservableObject {
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date $0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
} }
guard !proxyEntries.isEmpty else { return } guard !proxyEntries.isEmpty else { return }
log(" --- 代理日志 ---") log(String(localized: " --- 代理日志 ---"))
for e in proxyEntries { for e in proxyEntries {
log(" " + e.message) log(" " + e.localizedMessage)
} }
} }
} }
+5 -4
View File
@@ -229,10 +229,10 @@ Current client status:
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
the contributor requests anonymous inclusion. the contributor requests anonymous inclusion.
Module snapshots and provenance: Modules and scripts are now hosted by this repository. The original
`Yu9191/wloc` repository was deleted and remains attribution only, not a runtime dependency:
- [Third-party module documentation](docs/THIRD_PARTY_MODULES.md) - [Third-party module documentation](docs/THIRD_PARTY_MODULES.md)
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
scripts before importing them. scripts before importing them.
@@ -364,7 +364,7 @@ Deploy it to a test device using your own signing and installation process.
- Runtime logs remain in the device App Group container and retain only the latest three days; - Runtime logs remain in the device App Group container and retain only the latest three days;
- Issue reports are copied by the user before being submitted to GitHub; - Issue reports are copied by the user before being submitted to GitHub;
- App Mode accesses the local proxy and the environment-verification URL; - App Mode accesses the local proxy and the environment-verification URL;
- Third-party Proxy Mode may access the upstream module URL and the WLOC configuration endpoint; - Third-party Proxy Mode may access module URLs hosted by this repository and the WLOC configuration endpoint;
- The CA private key generated by the app is stored in the device Keychain; - The CA private key generated by the app is stored in the device Keychain;
- Third-party MITM, certificates, and proxy behavior are owned by the selected client. - Third-party MITM, certificates, and proxy behavior are owned by the selected client.
@@ -443,7 +443,8 @@ guarantee for every app or release.
## Acknowledgements and Links ## Acknowledgements and Links
The core location-response handling approach, Go implementation, and third-party modules are based on: The core location-response handling approach, Go implementation, and third-party modules are based on
the following projects (`Yu9191/wloc` has since been deleted):
- [Yu9191/wloc](https://github.com/Yu9191/wloc) - [Yu9191/wloc](https://github.com/Yu9191/wloc)
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer) - [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
+3 -4
View File
@@ -219,10 +219,9 @@ App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。 社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
相关模块快照和来源记录: 模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖:
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md) - [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。 第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
@@ -351,7 +350,7 @@ dist/PaopaoLocationSpoofer-unsigned.ipa
- 运行日志保存在设备 App Group 容器中,并自动保留近三天; - 运行日志保存在设备 App Group 容器中,并自动保留近三天;
- 问题报告需要用户主动复制后提交到 GitHub; - 问题报告需要用户主动复制后提交到 GitHub;
- APP 模式会访问本机代理和环境验证地址; - APP 模式会访问本机代理和环境验证地址;
- 第三方代理模式可能访问上游模块地址和 WLOC 配置接口; - 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
- App 生成的 CA 私钥保存在设备 Keychain 中; - App 生成的 CA 私钥保存在设备 Keychain 中;
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。 - 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
@@ -430,7 +429,7 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
## 致谢与友链 ## 致谢与友链
核心定位响应处理思路、Go 实现和第三方模块参考自: 核心定位响应处理思路、Go 实现和第三方模块参考自(`Yu9191/wloc` 原仓库已删除):
- [Yu9191/wloc](https://github.com/Yu9191/wloc) - [Yu9191/wloc](https://github.com/Yu9191/wloc)
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer) - [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
@@ -0,0 +1,11 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
[rewrite_local]
^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc url script-response-body https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) url script-echo-response https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[mitm]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+19
View File
@@ -0,0 +1,19 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!openUrl=https://wloc-pages.pages.dev/
[Argument]
longitude = input, "113.94114", tag=经度(在线选点优先)
latitude = input, "22.544577", tag=纬度(在线选点优先)
accuracy = input, "25", tag=精度(米)
randomRadius = input, "0", tag=扰动半径(米,0为关闭)
logLevel = select, "info", "off", "error", "warn", "debug", "all", tag=日志级别
[Script]
http-response ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, requires-body=true, binary-body-mode=true, timeout=30, tag=Apple WLOC, argument=[{longitude},{latitude},{accuracy},{randomRadius},{logLevel}]
http-request ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7, timeout=10, tag=WLOC Settings
[MITM]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
@@ -0,0 +1,13 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 (Shadowrocket 小火箭) | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=Yu9191 Rewrite
#!homepage=https://github.com/xweiba/location-spoofer
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
#!category=Tools
[Script]
Apple WLOC = type=http-response,pattern=^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc,requires-body=1,binary-body-mode=1,max-size=0,timeout=30,script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7,argument=longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
WLOC Settings = type=http-request,pattern=^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version),requires-body=0,max-size=0,timeout=10,script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
@@ -0,0 +1,14 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!category=Tools
#!arguments=经度:113.94114, 纬度:22.544577, 精度:25, 扰动半径:0, 日志级别:info
#!arguments-desc=经度/纬度: 默认坐标(在线选点储存后优先)\n精度: GPS精度(米)\n扰动半径: 每次响应在目标点周围随机偏移的最大距离(米),0为关闭\n日志级别: off/error/warn/info/debug/all\n\n使用方法: 打开选点页面 -> 选位置 -> 储存到设备
[Script]
Apple WLOC = type=http-response, pattern="^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc", requires-body=1, binary-body-mode=1, max-size=0, timeout=30, script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)", requires-body=0, max-size=0, timeout=10, script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
@@ -0,0 +1,35 @@
name: Apple WLOC 定位修改
desc: "修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/"
author: xweiba
homepage: https://github.com/xweiba/location-spoofer
category: Tools
http:
mitm:
- "gs-loc.apple.com"
- "gs-loc-cn.apple.com"
- "gsp-ssl.ls.apple.com"
- "bluedot.is.autonavi.com"
- "bluedot.is.autonavi.com.gds.alibabadns.com"
script:
- match: ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc
name: WLOC.Location
type: response
require-body: true
binary-mode: true
max-size: 0
timeout: 30
argument: longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
- match: ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)
name: WLOC.Settings
type: request
require-body: false
timeout: 10
script-providers:
WLOC.Location:
url: https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
interval: 86400
WLOC.Settings:
url: https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
interval: 86400
+368 -1
View File
@@ -142,6 +142,12 @@
"第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present"; "第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present";
"第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet"; "第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet";
"第三方模块连接失败" = "Third-party module connection failed"; "第三方模块连接失败" = "Third-party module connection failed";
"======== 第三方代理连接检测 ========" = "======== Third-party Proxy Connection Check ========";
"模式: 测试模式" = "Mode: Test mode";
"请求: wloc-settings/save?action=query" = "Request: wloc-settings/save?action=query";
"拦截响应: 有效 JSON" = "Intercepted response: Valid JSON";
"已保存坐标: %@" = "Saved coordinate: %@";
"结果: %@" = "Result: %@";
/* MARK: - TipViews */ /* MARK: - TipViews */
"生效说明" = "How to Activate"; "生效说明" = "How to Activate";
@@ -186,7 +192,7 @@
"去提交" = "Submit"; "去提交" = "Submit";
"复制模板" = "Copy Template"; "复制模板" = "Copy Template";
"不再提示" = "Don't Show Again"; "不再提示" = "Don't Show Again";
"第三方客户端" = "third-party client"; "第三方客户端" = "Third-party client";
"你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" = "You are using %@. Tapping “Submit” first copies the submission template and opens the community page inside the app. Accepted submissions are added to the README; you can choose to stay anonymous."; "你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" = "You are using %@. Tapping “Submit” first copies the submission template and opens the community page inside the app. Accepted submissions are added to the README; you can choose to stay anonymous.";
"已复制投稿模板" = "Submission Template Copied"; "已复制投稿模板" = "Submission Template Copied";
"如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。" = "If the template is not filled in automatically after you sign in to GitHub or the browser switches, just paste it manually."; "如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。" = "If the template is not filled in automatically after you sign in to GitHub or the browser switches, just paste it manually.";
@@ -330,6 +336,367 @@
"第三方代理清除坐标失败" = "The third-party proxy failed to clear the coordinate"; "第三方代理清除坐标失败" = "The third-party proxy failed to clear the coordinate";
"已有第三方代理请求正在执行" = "A third-party proxy request is already running"; "已有第三方代理请求正在执行" = "A third-party proxy request is already running";
"配置已提供,尚未验证" = "Configuration provided, not yet verified"; "配置已提供,尚未验证" = "Configuration provided, not yet verified";
"已连接,有保存坐标" = "Connected, coordinate saved";
"已连接,无保存坐标" = "Connected, no saved coordinate";
"连接失败(%@)" = "Connection failed (%@)";
"当前客户端:%@" = "Current client: %@";
"配置接口:/wloc-settings/save" = "Configuration endpoint: /wloc-settings/save";
"请求动作:WLOC query" = "Request action: WLOC query";
"检查范围:模块拦截、MITM、证书、代理/VPN 连接" = "Check scope: module interception, MITM, certificate, and proxy/VPN connection";
"连接状态:%@" = "Connection status: %@";
"检测结果:失败" = "Check result: Failed";
"耗时:%lld ms" = "Duration: %lld ms";
"错误类型:%@" = "Error type: %@";
"错误详情:%@" = "Error details: %@";
"处理建议:%@。" = "Suggested action: %@.";
"======== 第三方代理运行检测 ========" = "======== Third-party Proxy Runtime Check ========";
"触发来源:地图或设置中的第三方代理操作" = "Triggered by: third-party proxy action from the map or Settings";
"请求动作:WLOC 配置接口" = "Request action: WLOC configuration endpoint";
"处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。" = "Suggested action: confirm the module is enabled, then check MITM, the certificate, and the proxy/VPN connection.";
/* MARK: - Exported diagnostics */
"环境信息" = "Environment";
"App 版本" = "App version";
"系统版本" = "System version";
"诊断日志" = "Diagnostic Logs";
"不适用" = "Not applicable";
"是" = "Yes";
"否" = "No";
"(无诊断数据)" = "(No diagnostic data)";
"第三方代理测试模式:模块连接成功;已保存坐标=%@" = "Third-party proxy test mode: module connected; saved coordinate=%@";
"第三方代理测试模式:模块连接失败;%@" = "Third-party proxy test mode: module connection failed; %@";
"======== 代理验证测试 ========" = "======== Proxy Verification Test ========";
"App 版本: %@" = "App version: %@";
"系统版本: iOS %@" = "System version: iOS %@";
"[步骤 A] 检查代理是否运行…" = "[Step A] Check whether the proxy is running…";
" 端口: 127.0.0.1:8888" = " Port: 127.0.0.1:8888";
" ⚠ 代理未运行,尝试启动…" = " ⚠ Proxy is not running; attempting to start it…";
" ✗ 启动失败: %@" = " ✗ Failed to start: %@";
" ✓ 代理启动成功" = " ✓ Proxy started";
" ✓ 代理已在运行中" = " ✓ Proxy is already running";
"[步骤 B] 检测证书与 WiFi 代理…" = "[Step B] Check certificate and Wi-Fi proxy…";
" 方式: 请求 baidu.com/paopao-verify-<token>" = " Method: request baidu.com/paopao-verify-<token>";
" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过" = " Criteria: TLS error=certificate issue / response mismatch=proxy not configured / match=passed";
" ✗ 无法生成验证 token" = " ✗ Could not generate a verification token";
" ✓ 证书已信任,WiFi 代理已配置" = " ✓ Certificate is trusted and the Wi-Fi proxy is configured";
" ✗ 响应不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置" = " ✗ Response mismatch: HTTP %1$lld, %2$lld bytes; Wi-Fi proxy is not configured";
" ✗ 请求失败 [%@ code=%lld]: %@" = " ✗ Request failed [%1$@ code=%2$lld]: %3$@";
" TLS/证书校验失败,CA 证书未信任" = " TLS/certificate validation failed; the CA certificate is not trusted";
"======== 环境检测通过 ✓ ========" = "======== Environment Check Passed ✓ ========";
" --- 代理日志 ---" = " --- Proxy Logs ---";
/* MARK: - Setup examples and acknowledgements */
"成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}" = "Success: {\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\nFailure: {\"success\":false,\"error\":\"Error details\"}";
"定位改写脚本源自 Yu9191/wloc,现由本项目维护" = "Location-rewrite scripts originated from Yu9191/wloc and are now maintained by this project";
/* MARK: - Runtime log categories and messages */
"地图" = "Map";
"坐标转换" = "Coordinate Conversion";
"定位" = "Location";
"实时定位" = "Live Location";
"提醒" = "Reminder";
"搜索" = "Search";
"收藏" = "Favorites";
"缩放" = "Zoom";
"========== App 启动 ==========" = "========== App Launch ==========";
"尚未选择运行模式,跳过本地 CA 和代理初始化" = "No runtime mode selected; skipping local CA and proxy initialization";
"第三方代理测试模式:跳过本地 CA、代理和环境检测" = "Third-party proxy test mode: skipping local CA, proxy, and environment checks";
"旧坐标数据迁移失败,将在下次启动重试" = "Legacy coordinate migration failed; it will be retried on the next launch";
"地图坐标标准初始化完成,开始后续启动流程" = "Map coordinate-system initialization completed; continuing app startup";
"没有持久化图钉,地图创建前请求实时定位" = "No persisted pin; requesting live location before creating the map";
"已使用实时定位准备唯一初始地图状态" = "Prepared the initial map state using live location";
"地图创建前取得的初始实时位置(WGS-84)" = "Initial live location obtained before map creation (WGS-84)";
"地图创建前无法取得实时定位,唯一初始位置使用深圳" = "Could not obtain live location before map creation; using Shenzhen as the initial location";
"已找到持久化图钉,直接准备唯一初始地图状态" = "Found a persisted pin and prepared the initial map state";
"启动门禁全部完成,现在创建 MapHomeView" = "All startup gates completed; creating MapHomeView";
"初始化" = "Initialize";
"启动代理 127.0.0.1:8888" = "Starting proxy at 127.0.0.1:8888";
"启动代理: 恢复上次 WGS-84 定位" = "Starting proxy: restoring the last WGS-84 location";
"启动成功" = "Started successfully";
"启动失败" = "Failed to start";
"写入坐标" = "Write coordinate";
"跳过过期坐标写入" = "Skipped stale coordinate write";
"跳过旧验证坐标恢复" = "Skipped restoring coordinates from an outdated verification";
"运动状态模拟设置已更新" = "Motion simulation setting updated";
"准备证书下载失败" = "Failed to prepare certificate download";
"设置虚拟定位坐标" = "Set simulated-location coordinate";
"apply失败" = "Apply failed";
"保存收藏失败" = "Failed to save favorite";
"本地服务初始化失败" = "Local service initialization failed";
"代理运行模式已切换" = "Proxy runtime mode changed";
"已迁移旧版模式初始化状态" = "Migrated legacy mode initialization state";
"地图坐标标准检测已有请求进行中" = "A map coordinate-system check is already running";
"地图坐标标准检测开始" = "Map coordinate-system check started";
"地图坐标标准检测获得明确结果" = "Map coordinate-system check returned a definitive result";
"地图坐标标准检测不可用,开始实时定位兜底" = "Map coordinate-system check unavailable; starting live-location fallback";
"地图坐标标准检测使用兜底结果" = "Map coordinate-system check used the fallback result";
"地图坐标标准检测被取消,保留默认国内标准" = "Map coordinate-system check was cancelled; keeping the default mainland-China system";
"地图坐标标准已确定,允许创建地图" = "Map coordinate system determined; map creation is now allowed";
"地图坐标标准运行期检测合并到进行中请求" = "Runtime map coordinate-system check joined the request already in progress";
"地图坐标标准运行期检测开始" = "Runtime map coordinate-system check started";
"地图坐标标准运行期检测结果已过期,取消写入" = "Runtime map coordinate-system result became stale; discarding it";
"地图坐标标准运行期检测完成,标准未变化" = "Runtime map coordinate-system check completed with no change";
"地图坐标标准运行期检测发现切换" = "Runtime map coordinate-system check detected a change";
"地图坐标标准运行期检测失败,保留当前标准" = "Runtime map coordinate-system check failed; keeping the current system";
"地图坐标标准运行期检测已取消" = "Runtime map coordinate-system check cancelled";
"实时定位不覆盖固定锚点的明确检测结果" = "Live location did not override the definitive fixed-anchor result";
"实时定位确认兜底地图坐标标准无需修正" = "Live location confirmed that the fallback map coordinate system needs no correction";
"实时定位修正启动兜底地图坐标标准" = "Live location corrected the startup fallback map coordinate system";
"第三方代理已保存 WGS-84 坐标" = "Third-party proxy saved the WGS-84 coordinate";
"第三方代理坐标已清除" = "Third-party proxy coordinate cleared";
"第三方代理请求失败" = "Third-party proxy request failed";
"第三方代理坐标同步成功" = "Third-party proxy coordinate synchronized";
"同步坐标到第三方客户端失败" = "Failed to synchronize the coordinate to the third-party client";
"验证结果" = "Verification result";
"验证失败" = "Verification failed";
"开启前检测失败,进入对应环境引导" = "Pre-start check failed; opening the relevant setup guide";
"清除第三方客户端坐标失败" = "Failed to clear the coordinate from the third-party client";
"累计虚拟定位操作次数" = "Counted simulated-location operations";
"已复制坐标" = "Coordinate copied";
"地图坐标标准切换时未找到当前选点缓存" = "No current-selection cache was found when the map coordinate system changed";
"地图坐标标准切换后已使用缓存坐标对回显当前选点" = "Restored the current selection from cached coordinates after the map coordinate system changed";
"地图坐标类型已变化" = "Map coordinate type changed";
"取消保存收藏:检测期间当前选点已变化" = "Cancelled saving favorite because the current selection changed during detection";
"保存当前选点为收藏" = "Saving the current selection as a favorite";
"第三方代理查询返回失败" = "Third-party proxy query returned a failure";
"启动后第三方代理状态查询失败" = "Failed to query third-party proxy status after startup";
"检测到 Wi-Fi 网络变化" = "Detected a Wi-Fi network change";
"网络仍在变化,重新计算环境检测等待时间" = "Network is still changing; recalculating the environment-check delay";
"等待 Wi-Fi 连接稳定后检测" = "Waiting for the Wi-Fi connection to stabilize before checking";
"延时检测已被更新的网络事件取消" = "Delayed check was cancelled by a newer network event";
"稳定等待结束后仍未连接 Wi-Fi,提示检查代理" = "Wi-Fi is still disconnected after the stability delay; prompting the user to check the proxy";
"开始后台环境检测" = "Starting background environment check";
"已有环境检测运行,1 秒后重试" = "An environment check is already running; retrying in 1 second";
"后台环境检测完成" = "Background environment check completed";
"环境检测连续被占用,本次不重复弹窗" = "Environment check remained busy; suppressing a duplicate alert";
"用户点击实时定位" = "User requested live location";
"MapKit 蓝点尚不可用,启动 CLLocationManager 兜底" = "MapKit user location is unavailable; starting the CLLocationManager fallback";
"MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底" = "MapKit completed the request first; cancelling the CLLocationManager fallback";
"虚拟定位开启后的 MapKit 蓝点标准判定" = "Determined the MapKit user-location coordinate system after simulated location started";
"登记实时定位请求上下文" = "Registered live-location request context";
"复用进行中的 CLLocationManager 请求并更新意图上下文" = "Reusing the active CLLocationManager request and updating its intent context";
"CLLocationManager 兜底未返回坐标" = "CLLocationManager fallback returned no coordinate";
"丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成" = "Discarded CLLocationManager result because the task was cancelled or MapKit completed first";
"实时定位坐标完成标准判断并提交到地图" = "Live-location coordinate system determined and submitted to the map";
"MKLocalSearch 返回地点结果" = "MKLocalSearch returned a place result";
"反向地理编码网络失败,准备重试" = "Reverse geocoding network request failed; preparing to retry";
"反向地理编码失败" = "Reverse geocoding failed";
"获得搜索结果" = "Received search results";
"点击收藏点并回显到地图" = "Selected a favorite and displayed it on the map";
"忽略重复 CLLocationManager 请求" = "Ignored duplicate CLLocationManager request";
"CLLocationManager 请求入口" = "CLLocationManager request entry";
"授权状态不允许定位" = "Authorization status does not permit location access";
"创建 CLLocationManager 请求" = "Created CLLocationManager request";
"请求前台定位授权" = "Requesting foreground location authorization";
"定位授权状态变化" = "Location authorization status changed";
"CLLocationManager 返回样本批次" = "CLLocationManager returned a batch of samples";
"本批次没有可完成当前请求的样本" = "This batch contains no sample that can complete the current request";
"接受 CLLocationManager 样本并完成请求" = "Accepted CLLocationManager sample and completed the request";
"定位回调失败" = "Location callback failed";
"跳过 CLLocationManager 缓存样本" = "Skipped cached CLLocationManager sample";
"等待定位授权超时" = "Timed out waiting for location authorization";
"单次定位超时,切换持续定位" = "One-shot location timed out; switching to continuous updates";
"持续定位超时" = "Continuous location updates timed out";
"开始 CLLocationManager 单次定位" = "Starting one-shot CLLocationManager request";
"开始 CLLocationManager 持续定位兜底" = "Starting continuous CLLocationManager fallback";
"CLLocationManager 请求完成" = "CLLocationManager request completed";
"CLLocationManager 请求结束但没有坐标" = "CLLocationManager request ended without a coordinate";
"使用 CLLocationManager 新鲜缓存" = "Using a fresh CLLocationManager cache entry";
"实时定位按钮直接使用 MapKit 蓝点缓存" = "Live-location action used the MapKit user-location cache directly";
"主页收到待处理请求所需的 MapKit 蓝点回调" = "Home screen received the MapKit user-location callback for the pending request";
"主页收到 CLLocationManager 兜底坐标" = "Home screen received the CLLocationManager fallback coordinate";
"原始实时定位坐标" = "Original live-location coordinate";
"检查 CLLocationManager 样本" = "Checking CLLocationManager sample";
"拒绝 CLLocationManager 样本:坐标或精度无效" = "Rejected CLLocationManager sample because its coordinate or accuracy is invalid";
"MapKit 蓝点更新但 location 为空" = "MapKit user location updated but its location value is empty";
"拒绝 MapKit 蓝点样本:坐标无效" = "Rejected MapKit user-location sample because its coordinate is invalid";
"拒绝 MapKit 蓝点样本:水平精度无效" = "Rejected MapKit user-location sample because horizontal accuracy is invalid";
"音频中断恢复" = "Recovered from audio interruption";
"音频会话失败" = "Audio session failed";
"无法创建静音音频缓冲区" = "Could not create the silent-audio buffer";
"引擎启动失败" = "Audio engine failed to start";
"后台保活已启动(静音音频)" = "Background keep-alive started (silent audio)";
"后台保活已停止" = "Background keep-alive stopped";
"保存当前图钉失败" = "Failed to save the current pin";
"存储缩放" = "Stored zoom level";
"旧坐标数据迁移完成" = "Legacy coordinate migration completed";
"远程版本配置加载成功" = "Remote version configuration loaded";
"版本配置源不可用,尝试下一地址" = "Version configuration source unavailable; trying the next URL";
"版本配置加载失败,继续使用内置配置" = "Failed to load version configuration; continuing with built-in values";
"版本说明源不可用,尝试下一地址" = "Release-notes source unavailable; trying the next URL";
"版本说明加载失败,将使用最新 Release 页面" = "Failed to load release notes; using the latest Release page";
"复用设备钥匙串中的 CA" = "Reusing the CA from the device keychain";
"旧 CA 已迁移到设备钥匙串" = "Legacy CA migrated to the device keychain";
"已生成并保存设备专属 CA" = "Generated and saved a device-specific CA";
"已删除设备 CA,等待重新生成" = "Deleted the device CA; waiting to regenerate it";
"钥匙串中的 CA 无效,准备回退" = "CA in the keychain is invalid; preparing fallback";
"删除旧 CA 文件失败,将在下次启动重试" = "Failed to delete legacy CA files; it will be retried on the next launch";
"无法解析 CA 证书 PEM" = "Could not parse the CA certificate PEM";
"无法创建 SecTrust" = "Could not create SecTrust";
"SecTrust 评估返回错误" = "SecTrust evaluation returned an error";
"CA 证书已被系统信任" = "CA certificate is trusted by the system";
"CA 证书未被系统信任" = "CA certificate is not trusted by the system";
"调用 Go Core 生成 CA" = "Calling Go Core to generate the CA";
"Go Core CA 生成成功" = "Go Core generated the CA";
"本地证书服务已在运行" = "Local certificate server is already running";
"调用 Go Core 启动本地证书服务" = "Calling Go Core to start the local certificate server";
"本地证书服务启动成功" = "Local certificate server started";
"停止本地证书服务" = "Stopping local certificate server";
"开始第三方代理连接检测" = "Starting third-party proxy connection check";
"第三方代理连接检测通过" = "Third-party proxy connection check passed";
"第三方代理连接检测失败" = "Third-party proxy connection check failed";
"关闭代理前已同步关闭虚拟定位" = "Stopped simulated location before stopping the proxy";
"切换 APP 模式前无法清除第三方坐标" = "Could not clear the third-party coordinate before switching to App Mode";
"设置页第三方连接检测通过" = "Third-party connection check passed in Settings";
"设置页第三方连接检测失败" = "Third-party connection check failed in Settings";
"地图实际显示坐标" = "Coordinate displayed on map";
/* MARK: - Runtime log fields and values */
"App回到前台" = "App returned to foreground";
"App已确认地图标准" = "App confirmed the map coordinate system";
"CLLocationManager请求中" = "CLLocationManager request active";
"Go proxy 启动失败" = "Go proxy failed to start";
"MapKit地图蓝点" = "MapKit user location";
"MapKit蓝点新样本" = "New MapKit user-location sample";
"MapKit蓝点缓存" = "MapKit user-location cache";
"SSID可读取" = "SSID readable";
"WLOC写入标准" = "WLOC write coordinate system";
"WLOC目标标准" = "WLOC target coordinate system";
"Wi-Fi接口" = "Wi-Fi interface";
"intent选点revision" = "Intent selection revision";
"manager请求中" = "Manager request active";
"上下文存在" = "Context present";
"事件原因" = "Event reason";
"任务已取消" = "Task cancelled";
"任务已存在" = "Task already present";
"使用兜底" = "Used fallback";
"保存收藏" = "Save favorite";
"保存数据包含" = "Saved data contains";
"保持未启用" = "Kept disabled";
"保留原第三方坐标" = "Kept previous third-party coordinate";
"保留已启用状态" = "Kept enabled state";
"保留标准" = "Retained coordinate system";
"修正兜底标准" = "Corrected fallback coordinate system";
"内存缓存存在" = "In-memory cache present";
"初始坐标来源" = "Initial coordinate source";
"初始来源" = "Initial source";
"初始阶段" = "Initial phase";
"判定规则" = "Decision rule";
"原兜底来源" = "Original fallback source";
"原因" = "Reason";
"取值字段" = "Source field";
"可显示不再提醒" = "Can show suppress option";
"启动代理…" = "Starting proxy…";
"命中林士街" = "Matched Lin Shi Street";
"固定锚点" = "Fixed anchor";
"固定锚点查询超过5秒" = "Fixed-anchor query exceeded 5 seconds";
"固定锚点查询返回空结果" = "Fixed-anchor query returned no results";
"国内标准(GCJ-02)" = "Mainland China system (GCJ-02)";
"国内转换区域" = "Mainland conversion region";
"国外非转换区域" = "Non-conversion region outside mainland China";
"国际标准(WGS-84)" = "International system (WGS-84)";
"国际标准(WGS-84)+国内标准(GCJ-02)" = "International (WGS-84) + mainland China (GCJ-02)";
"图钉已按新类型重设" = "Pin reset for the new type";
"地图取值字段" = "Map source field";
"地图标准" = "Map coordinate system";
"坐标有效" = "Coordinate valid";
"坐标标准" = "Coordinate system";
"垂直精度米" = "Vertical accuracy (m)";
"基础校验通过" = "Basic validation passed";
"处理建议" = "Suggested action";
"失败时提示" = "Show alert on failure";
"实时定位存在" = "Live location present";
"实时定位服务区域" = "Live-location service region";
"客户端模式" = "Client mode";
"尝试" = "Attempt";
"已取消位置更新" = "Location updates cancelled";
"已恢复真实定位" = "Real location restored";
"已清理" = "Cleared";
"已重置" = "Reset";
"开启" = "Enable";
"异步地理编码" = "Asynchronous geocoding";
"当前地图标准" = "Current map coordinate system";
"当前客户端" = "Current client";
"当前标准" = "Current coordinate system";
"当前选点revision" = "Current selection revision";
"恢复状态" = "Restore state";
"批次索引" = "Batch index";
"持久化字段" = "Persisted field";
"授权状态" = "Authorization status";
"授权状态rawValue" = "Authorization status raw value";
"探测失败原因" = "Probe failure reason";
"搜索结果" = "Search results";
"操作" = "Operation";
"新类型" = "New type";
"无已保存" = "None saved";
"无法判定" = "Undetermined";
"无法启动本地证书服务" = "Could not start the local certificate server";
"无法生成本地证书" = "Could not generate the local certificate";
"日志策略" = "Logging policy";
"旧类型" = "Previous type";
"显示坐标字段" = "Displayed coordinate field";
"最低版本" = "Minimum version";
"最新版本" = "Latest version";
"最终标准" = "Final coordinate system";
"最终阶段" = "Final phase";
"有坐标" = "Coordinate present";
"有效样本数" = "Valid sample count";
"有缓存" = "Cache present";
"未知错误" = "Unknown error";
"本地 CA 证书或私钥无效" = "Local CA certificate or private key is invalid";
"无法写入设备钥匙串(%d)" = "Could not write to the device keychain (%d)";
"来源" = "Source";
"来源类型" = "Source type";
"林士街" = "Lin Shi Street";
"查看诊断日志" = "View diagnostic logs";
"样本年龄秒" = "Sample age (s)";
"样本数" = "Sample count";
"样本时间" = "Sample time";
"检查范围" = "Check scope";
"检测前标准" = "Coordinate system before check";
"模块拦截、MITM、代理/VPN连接" = "Module interception, MITM, and proxy/VPN connection";
"次数" = "Count";
"每次开启首次或判定变化" = "First sample after each start or classification change";
"水平精度米" = "Horizontal accuracy (m)";
"活动requestID" = "Active request ID";
"活动阶段" = "Active phase";
"海拔米" = "Altitude (m)";
"满足当前请求时间窗" = "Within current request time window";
"点击实时定位" = "Live-location action";
"目标所在区域" = "Target region";
"确认标准" = "Confirmed coordinate system";
"社区征集客户端数" = "Clients requesting community contributions";
"等待秒数" = "Wait duration (s)";
"系统缓存存在" = "System cache present";
"结果" = "Result";
"结果数" = "Result count";
"结果来源" = "Result source";
"缓存" = "Cache";
"缓存上限秒" = "Cache limit (s)";
"缓存时效通过" = "Cache freshness valid";
"缩放米" = "Zoom (m)";
"网络可用" = "Network available";
"耗时毫秒" = "Duration (ms)";
"蓝点回调更接近" = "User-location callback is closer";
"蓝点缓存" = "User-location cache";
"蓝点缓存存在" = "User-location cache present";
"蓝点距GCJ目标米" = "User-location distance to GCJ target (m)";
"蓝点距WGS目标米" = "User-location distance to WGS target (m)";
"触发原因" = "Trigger reason";
"诊断位置" = "Diagnostic location";
"请求动作" = "Request action";
"超时毫秒" = "Timeout (ms)";
"输入坐标标准" = "Input coordinate system";
"连接状态" = "Connection status";
"选点revision" = "Selection revision";
"选点期间发生变化" = "Selection changed during operation";
"错误" = "Error";
"错误类型" = "Error type";
"锚点" = "Anchor";
"阶段" = "Phase";
"首条名称" = "First result name";
"首条名称=林士街→GCJ-02,否则→WGS-84" = "First name=Lin Shi Street → GCJ-02; otherwise → WGS-84";
"默认国内标准" = "Default mainland-China coordinate system";
/* MARK: - ProxyManager */ /* MARK: - ProxyManager */
"证书下载地址无效" = "Invalid certificate download URL"; "证书下载地址无效" = "Invalid certificate download URL";
+2 -2
View File
@@ -13,8 +13,8 @@ enum CertificateAuthorityStoreError: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .invalidAuthority: return "本地 CA 证书或私钥无效" case .invalidAuthority: return String(localized: "本地 CA 证书或私钥无效")
case let .keychain(status): return "无法写入设备钥匙串(\(status))" case let .keychain(status): return String(localized: "无法写入设备钥匙串(\(status))")
} }
} }
} }
+2 -2
View File
@@ -12,8 +12,8 @@ enum CoreBridgeError: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .generationFailed: return "无法生成本地证书" case .generationFailed: return String(localized: "无法生成本地证书")
case .serverStartFailed: return "无法启动本地证书服务" case .serverStartFailed: return String(localized: "无法启动本地证书服务")
} }
} }
} }
+28 -3
View File
@@ -38,10 +38,35 @@ struct RuntimeLogEntry: Codable, Identifiable, Equatable {
var renderedText: String { var renderedText: String {
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let suffix = details.isEmpty let localizedDetails = details.map {
(localizedDiagnosticText($0.key), localizedDiagnosticValue($0.value))
}
let suffix = localizedDetails.isEmpty
? "" ? ""
: " " + details.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\($0.value)" }.joined(separator: " ") : " " + localizedDetails.sorted(by: { $0.0 < $1.0 }).map { "\($0.0)=\($0.1)" }.joined(separator: " ")
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(category)] \(message)\(suffix)" return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(localizedCategory)] \(localizedMessage)\(suffix)"
}
var localizedCategory: String { localizedDiagnosticText(category) }
var localizedMessage: String { localizedDiagnosticText(message) }
var localizedDetailsText: String {
details.map { (localizedDiagnosticText($0.key), localizedDiagnosticValue($0.value)) }
.sorted(by: { $0.0 < $1.0 })
.map { "\($0.0): \($0.1)" }
.joined(separator: "\n")
}
private func localizedDiagnosticText(_ text: String) -> String {
String(localized: String.LocalizationValue(text))
}
private func localizedDiagnosticValue(_ value: String) -> String {
switch value {
case "true": return String(localized: "是")
case "false": return String(localized: "否")
default: return localizedDiagnosticText(value)
}
} }
} }
+15 -16
View File
@@ -170,7 +170,10 @@ final class ThirdPartyProxyManager: ObservableObject {
URLQueryItem(name: "lon", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), longitude)), URLQueryItem(name: "lon", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), longitude)),
URLQueryItem(name: "lat", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), latitude)), URLQueryItem(name: "lat", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), latitude)),
URLQueryItem(name: "acc", value: String(accuracy)), URLQueryItem(name: "acc", value: String(accuracy)),
URLQueryItem(name: "randomRadius", value: String(randomRadius)) URLQueryItem(
name: "randomRadius",
value: String(format: "%g", locale: Locale(identifier: "en_US_POSIX"), randomRadius)
)
] ]
} }
guard let url = components.url else { throw ThirdPartyProxyError.invalidResponse } guard let url = components.url else { throw ThirdPartyProxyError.invalidResponse }
@@ -203,6 +206,9 @@ final class ThirdPartyProxyManager: ObservableObject {
} }
enum ThirdPartyProxyClient: String, CaseIterable, Identifiable { enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
/// Bump when a hosted module or script changes to invalidate proxy-client caches.
static let moduleSubscriptionVersion = "1.0.7"
case shadowrocket case shadowrocket
case surge case surge
case quantumultX case quantumultX
@@ -239,21 +245,14 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
@MainActor @MainActor
var subscriptionURL: URL { var subscriptionURL: URL {
// Third-party modules are served directly from the upstream Yu9191/wloc let directory = ThirdPartyModuleSourceStore.shared.useMirror
// repository (mirror via gh-proxy when enabled). We no longer maintain ? "Resources/ThirdPartyProxyModules"
// project-owned module copies, so the URL tracks upstream releases. : "ThirdParty/WlocScripts/modules/direct"
let fileName: String let prefix = ThirdPartyModuleSourceStore.shared.useMirror
switch self { ? "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
case .surge, .egern: fileName = "wloc.sgmodule" : "https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
case .quantumultX: fileName = "wloc.conf" let base = "\(prefix)\(directory)/\(moduleFileName)"
case .loon: fileName = "wloc.lpx" return URL(string: "\(base)?v=\(Self.moduleSubscriptionVersion)")!
case .stash: fileName = "wloc.stoverride"
case .shadowrocket: fileName = "wloc.module"
}
let base = ThirdPartyModuleSourceStore.shared.useMirror
? "https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/\(fileName)"
: "https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/\(fileName)"
return URL(string: base)!
} }
var launchURL: URL? { var launchURL: URL? {
@@ -73,9 +73,13 @@ final class RealtimeLocationManagerTests: XCTestCase {
func testOneShotTimeoutTransitionsToContinuousFallback() async { func testOneShotTimeoutTransitionsToContinuousFallback() async {
let driver = FakeRealtimeLocationDriver() let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 5_000_000, fallbackTimeoutNanoseconds: 1_000_000_000) let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 5_000_000, fallbackTimeoutNanoseconds: 1_000_000_000)
let fallbackStarted = expectation(description: "Continuous location fallback started")
driver.onStartUpdating = {
fallbackStarted.fulfill()
}
let request = Task { await manager.requestLocation() } let request = Task { await manager.requestLocation() }
try? await Task.sleep(nanoseconds: 20_000_000) await fulfillment(of: [fallbackStarted], timeout: 1)
XCTAssertEqual(driver.startUpdatingCallCount, 1) XCTAssertEqual(driver.startUpdatingCallCount, 1)
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40)) driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
@@ -148,6 +152,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
weak var delegate: CLLocationManagerDelegate? weak var delegate: CLLocationManagerDelegate?
var onRequestLocation: (() -> Void)? var onRequestLocation: (() -> Void)?
var onStartUpdating: (() -> Void)?
private(set) var requestAuthorizationCallCount = 0 private(set) var requestAuthorizationCallCount = 0
private(set) var requestLocationCallCount = 0 private(set) var requestLocationCallCount = 0
private(set) var startUpdatingCallCount = 0 private(set) var startUpdatingCallCount = 0
@@ -164,6 +169,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
func startUpdatingLocation() { func startUpdatingLocation() {
startUpdatingCallCount += 1 startUpdatingCallCount += 1
onStartUpdating?()
} }
func stopUpdatingLocation() { func stopUpdatingLocation() {
@@ -125,16 +125,19 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
} }
} }
func testClientLinksUseUpstreamModulesAndVerificationLabels() { func testClientLinksUseProjectOwnedModulesAndVerificationLabels() {
XCTAssertEqual( XCTAssertEqual(
ThirdPartyProxyManager.interceptionHostnamesText, ThirdPartyProxyManager.interceptionHostnamesText,
"gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com" "gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com"
) )
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText) XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
XCTAssertTrue(ThirdPartyProxyClient.surge.verificationText?.contains("尚未验证") == true) XCTAssertEqual(
ThirdPartyProxyClient.surge.verificationText,
String(localized: "配置已提供,尚未验证")
)
XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL) XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL)
XCTAssertTrue(ThirdPartyProxyClient.stash.subscriptionURL.absoluteString.hasPrefix( XCTAssertTrue(ThirdPartyProxyClient.stash.subscriptionURL.absoluteString.hasPrefix(
"https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/" "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/"
)) ))
let stashComponents = URLComponents( let stashComponents = URLComponents(
url: ThirdPartyProxyClient.stash.subscriptionURL, url: ThirdPartyProxyClient.stash.subscriptionURL,
@@ -144,10 +147,10 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
url: ThirdPartyProxyClient.shadowrocket.subscriptionURL, url: ThirdPartyProxyClient.shadowrocket.subscriptionURL,
resolvingAgainstBaseURL: false resolvingAgainstBaseURL: false
) )
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.stoverride") XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.stoverride")
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.module") XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.module")
XCTAssertTrue(stashComponents?.queryItems?.isEmpty ?? true) XCTAssertEqual(stashComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
XCTAssertTrue(shadowrocketComponents?.queryItems?.isEmpty ?? true) XCTAssertEqual(shadowrocketComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket") XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket")
XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge") XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge")
XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x") XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x")
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STRINGS="$ROOT/Resources/en.lproj/Localizable.strings"
fail() {
echo "FAIL: $1" >&2
exit 1
}
plutil -lint "$STRINGS" >/dev/null || fail "English localization file is invalid"
ruby - "$STRINGS" <<'RUBY' || exit 1
path = ARGV.fetch(0)
entries = File.readlines(path).map do |line|
match = line.match(/^"((?:\\.|[^"])*)"\s*=\s*"((?:\\.|[^"])*)";/)
[match[1], match[2]] if match
end.compact
duplicates = entries.group_by(&:first).select { |_key, values| values.length > 1 }.keys
abort "FAIL: duplicate English localization keys: #{duplicates.join(', ')}" unless duplicates.empty?
placeholder = /%(?:\d+\$)?(?:@|lld|ld|d|f)/
entries.each do |source, target|
source_count = source.scan(placeholder).length
target_count = target.scan(placeholder).length
abort "FAIL: placeholder mismatch for #{source.inspect}" unless source_count == target_count
end
RUBY
for key in \
'环境信息' \
'诊断日志' \
'======== 代理验证测试 ========' \
'======== 第三方代理连接检测 ========' \
'======== 第三方代理运行检测 ========' \
'第三方代理测试模式:模块连接成功;已保存坐标=%@' \
'成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}'; do
grep -Fq "\"$key\" = " "$STRINGS" || fail "missing critical English localization: $key"
done
grep -q 'localizedCategory' "$ROOT/Shared/RuntimeLog.swift" \
|| fail "runtime log categories must be localized when rendered"
grep -q 'localizedDetailsText' "$ROOT/App/DiagnosticsView.swift" \
|| fail "runtime log details must be localized in the diagnostics UI"
grep -q 'String(localized: "诊断日志")' "$ROOT/App/BugReportView.swift" \
|| fail "generated bug reports must localize their diagnostic section"
grep -q 'log(" " + e.localizedMessage)' "$ROOT/App/SetupCoordinator.swift" \
|| fail "bug-report verification logs must render stored messages in the active language"
if grep -R -n 'raw.githubusercontent.com/Yu9191/wloc' \
"$ROOT/App" "$ROOT/Shared" "$ROOT/Resources/ThirdPartyProxyModules" \
"$ROOT/ThirdParty/WlocScripts/modules"; then
fail "deleted Yu9191/wloc repository must not remain a runtime dependency"
fi
echo "PASS: localization and diagnostic export contract"
+20 -5
View File
@@ -10,8 +10,8 @@ CONTENT="$ROOT/App/ContentView.swift"
SETUP="$ROOT/App/FirstSetupView.swift" SETUP="$ROOT/App/FirstSetupView.swift"
SETTINGS="$ROOT/App/SettingsView.swift" SETTINGS="$ROOT/App/SettingsView.swift"
grep -q 'return "APP模式"' "$MODE" || fail "APP mode display name is missing" grep -q 'String(localized: "APP模式")' "$MODE" || fail "localized APP mode display name is missing"
grep -q 'return "第三方代理模式"' "$MODE" || fail "third-party mode display name is missing" grep -q 'String(localized: "第三方代理模式")' "$MODE" || fail "localized third-party mode display name is missing"
grep -q 'hasSelectedMode' "$MODE" || fail "first-launch mode selection must be persisted" grep -q 'hasSelectedMode' "$MODE" || fail "first-launch mode selection must be persisted"
grep -q 'guard runtimeMode.hasSelectedMode else' "$CONTENT" || fail "mode selection must gate startup" grep -q 'guard runtimeMode.hasSelectedMode else' "$CONTENT" || fail "mode selection must gate startup"
grep -q 'phase = .setup' "$CONTENT" || fail "first launch must enter setup before map construction" grep -q 'phase = .setup' "$CONTENT" || fail "first launch must enter setup before map construction"
@@ -37,8 +37,23 @@ grep -Fq '保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>' "$SETUP" \
grep -Fq '清除:GET ?action=clear' "$SETUP" \ grep -Fq '清除:GET ?action=clear' "$SETUP" \
|| fail "client integration guidance must document the clear action" || fail "client integration guidance must document the clear action"
grep -q 'Yu9191/wloc/refs/heads/main/modules' "$MANAGER" \ grep -q 'raw.githubusercontent.com/xweiba/location-spoofer/main' "$MANAGER" \
|| fail "third-party subscription must point at upstream Yu9191 modules" || fail "third-party subscription must point at project-owned modules"
! grep -q 'raw.githubusercontent.com/Yu9191/wloc' "$MANAGER" \
|| fail "third-party subscription must not depend on the removed upstream repository"
MODULES="$ROOT/Resources/ThirdPartyProxyModules"
SCRIPTS="$ROOT/ThirdParty/WlocScripts"
for file in wloc.module wloc.sgmodule wloc.conf wloc.lpx wloc.stoverride; do
test -s "$MODULES/$file" || fail "missing project-owned mirrored module: $file"
test -s "$SCRIPTS/modules/direct/$file" || fail "missing project-owned direct module: $file"
! grep 'ThirdParty/WlocScripts/dist/v1/wloc' "$MODULES/$file" "$SCRIPTS/modules/direct/$file" \
| grep -Fvq '?v=1.0.7' \
|| fail "hosted script URLs must carry the current cache version: $file"
done
test -s "$SCRIPTS/dist/v1/wloc.js" || fail "missing hosted WLOC response script"
test -s "$SCRIPTS/dist/v1/wloc-settings.js" || fail "missing hosted WLOC settings script"
! grep -R -q 'raw.githubusercontent.com/Yu9191/wloc' "$MODULES" "$SCRIPTS/modules/direct" \
|| fail "hosted modules must not reference the removed upstream repository"
grep -q 'wloc.sgmodule' "$MANAGER" || fail "Surge/Egern module mapping is missing" grep -q 'wloc.sgmodule' "$MANAGER" || fail "Surge/Egern module mapping is missing"
grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly" grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly"
grep -q 'shadowrocket://' "$MANAGER" || fail "Shadowrocket launch URL is missing" grep -q 'shadowrocket://' "$MANAGER" || fail "Shadowrocket launch URL is missing"
@@ -88,7 +103,7 @@ grep -q 'setup.requestThirdPartySetup(message: error.localizedDescription)' "$RO
|| fail "third-party coordinate sync failures must open the import guide" || fail "third-party coordinate sync failures must open the import guide"
grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \ grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \
|| fail "runtime repair must explain why the import guide opened" || fail "runtime repair must explain why the import guide opened"
test "$(grep -c 'title: \"接口连接失败\"' "$SETUP")" -eq 1 \ test "$(grep -c 'title: String(localized: \"接口连接失败\")' "$SETUP")" -eq 1 \
|| fail "third-party failure details must render in one shared result area" || fail "third-party failure details must render in one shared result area"
grep -Fq '当前客户端:\(client.name)' "$SETUP" \ grep -Fq '当前客户端:\(client.name)' "$SETUP" \
|| fail "third-party failure logs must identify the selected client" || fail "third-party failure logs must identify the selected client"
@@ -25,7 +25,7 @@ import sys
with open(sys.argv[1], encoding="utf-8") as handle: with open(sys.argv[1], encoding="utf-8") as handle:
config = json.load(handle) config = json.load(handle)
assert config["latestVersion"] == "1.0.5" assert config["latestVersion"] == "1.0.6"
assert config["minimumSupportedVersion"] == "1.0.0" assert config["minimumSupportedVersion"] == "1.0.0"
assert "shadowrocket" not in config["communityPromptClients"] assert "shadowrocket" not in config["communityPromptClients"]
assert set(config["communityPromptClients"]) == { assert set(config["communityPromptClients"]) == {
@@ -121,7 +121,7 @@ grep -Fq 'SafariView(url: destination.url)' "$BUG_REPORT" \
|| fail "the App bug report must not be handed to an external GitHub client" || fail "the App bug report must not be handed to an external GitHub client"
grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \ grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \
|| fail "the App must tell users where to paste the generated report" || fail "the App must tell users where to paste the generated report"
grep -Fq '第三方客户端:' "$BUG_REPORT" \ grep -Fq 'String(localized: "第三方客户端")' "$BUG_REPORT" \
|| fail "the generated report must identify the selected third-party client" || fail "the generated report must identify the selected third-party client"
grep -Fq 'Label("报告 Bug"' "$SETTINGS" \ grep -Fq 'Label("报告 Bug"' "$SETTINGS" \
|| fail "Settings must identify the support action as a bug report" || fail "Settings must identify the support action as a bug report"
+6
View File
@@ -0,0 +1,6 @@
# Third-Party Notices
The generated proxy scripts bundle:
- `pako` 2.1.0, Copyright (C) 2014-2017 Vitaly Puzrin and contributors, MIT License.
- `esbuild` is used only as a development/build dependency and is distributed under the MIT License.
+19
View File
@@ -0,0 +1,19 @@
import { build } from "esbuild";
import { mkdir } from "node:fs/promises";
await mkdir(new URL("./dist/v1/", import.meta.url), { recursive: true });
for (const [entry, outfile] of [
["src/response-entry.js", "dist/v1/wloc.js"],
["src/settings-entry.js", "dist/v1/wloc-settings.js"]
]) {
await build({
entryPoints: [entry],
outfile,
bundle: true,
format: "iife",
target: ["es2017"],
minify: true,
legalComments: "eof"
});
}
+1
View File
@@ -0,0 +1 @@
(()=>{var y="1.0.0";var m=["wifi","cellTower","arpc","marker","synthetic","bare","motionSimulation"],c="locationSpoofer.settings.v1";function u(){return typeof $task!="undefined"?"quantumultX":typeof $loon!="undefined"?"loon":typeof $rocket!="undefined"?"shadowrocket":typeof Egern!="undefined"?"egern":typeof $environment!="undefined"&&$environment["stash-version"]?"stash":typeof $environment!="undefined"&&$environment["surge-version"]?"surge":"unknown"}function h(e){let t=u()==="quantumultX"?$prefs.valueForKey(e):$persistentStore.read(e);if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function l(e,t){let n=t==null?"":JSON.stringify(t);return u()==="quantumultX"?$prefs.setValueForKey(n,e):$persistentStore.write(n,e)}function i(e){let t={status:200,headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify(e)};u()==="quantumultX"?$done(Object.assign({},t,{status:"HTTP/1.1 200 OK"})):u()==="stash"?$done(t):$done({response:t})}function g(e){let t=e.split("?")[1]||"",n={};return t.split("&").forEach(s=>{if(!s)return;let o=s.indexOf("="),f=o<0?s:s.slice(0,o),d=o<0?"":s.slice(o+1),a=f,p=d;try{a=decodeURIComponent(f.replace(/\+/g," "))}catch($){}try{p=decodeURIComponent(d.replace(/\+/g," "))}catch($){}Object.prototype.hasOwnProperty.call(n,a)||(n[a]=p)}),n}function O(e){let t=e.split("?")[0],n=t.indexOf("://");if(n<0)return t;let s=t.indexOf("/",n+3);return s<0?"/":t.slice(s)}var b=typeof $request=="undefined"?"":$request.url||"",E=O(b),r=g(b);if(E==="/wloc-settings/version")i({success:!0,moduleVersion:y,protocolVersion:1,capabilities:m});else if(r.action==="query"){let e=h(c);i(e&&e.enabled?{success:!0,longitude:e.longitude,latitude:e.latitude,accuracy:e.accuracy,motionSimulationEnabled:e.motionSimulationEnabled===!0}:{success:!1,error:"\u65E0\u5DF2\u4FDD\u5B58\u7684\u5750\u6807"})}else if(r.action==="clear")l(c,null),i({success:!0});else{let e=Number(r.lon!=null?r.lon:r.longitude),t=Number(r.lat!=null?r.lat:r.latitude),n=Number(r.acc!=null?r.acc:r.accuracy!=null?r.accuracy:25);if(!Number.isFinite(e)||!Number.isFinite(t))i({success:!1,error:"\u7F3A\u5C11 lon/lat \u53C2\u6570"});else{let s={enabled:!0,longitude:e,latitude:t,accuracy:n,motionSimulationEnabled:r.motion==="1"},o=l(c,s);i(o?{success:!0,longitude:e,latitude:t,accuracy:n,motionSimulationEnabled:s.motionSimulationEnabled}:{success:!1,error:"\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25"})}}})();
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
[rewrite_local]
^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc url script-response-body https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) url script-echo-response https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[mitm]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+18
View File
@@ -0,0 +1,18 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
[Argument]
longitude = input, "113.94114", tag=经度(在线选点优先)
latitude = input, "22.544577", tag=纬度(在线选点优先)
accuracy = input, "25", tag=精度(米)
randomRadius = input, "0", tag=扰动半径(米,0为关闭)
logLevel = select, "info", "off", "error", "warn", "debug", "all", tag=日志级别
[Script]
http-response ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, requires-body=true, binary-body-mode=true, timeout=30, tag=Apple WLOC, argument=[{longitude},{latitude},{accuracy},{randomRadius},{logLevel}]
http-request ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7, timeout=10, tag=WLOC Settings
[MITM]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+13
View File
@@ -0,0 +1,13 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 (Shadowrocket 小火箭) | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=Yu9191 Rewrite
#!homepage=https://github.com/xweiba/location-spoofer
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
#!category=Tools
[Script]
Apple WLOC = type=http-response,pattern=^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc,requires-body=1,binary-body-mode=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7,argument=longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
WLOC Settings = type=http-request,pattern=^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version),requires-body=0,max-size=0,timeout=10,script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+14
View File
@@ -0,0 +1,14 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!category=Tools
#!arguments=经度:113.94114, 纬度:22.544577, 精度:25, 扰动半径:0, 日志级别:info
#!arguments-desc=经度/纬度: 默认坐标(在线选点储存后优先)\n精度: GPS精度(米)\n扰动半径: 每次响应在目标点周围随机偏移的最大距离(米),0为关闭\n日志级别: off/error/warn/info/debug/all\n\n使用方法: 打开选点页面 -> 选位置 -> 储存到设备
[Script]
Apple WLOC = type=http-response, pattern="^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc", requires-body=1, binary-body-mode=1, max-size=0, timeout=30, script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)", requires-body=0, max-size=0, timeout=10, script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+35
View File
@@ -0,0 +1,35 @@
name: Apple WLOC 定位修改
desc: Location Spoofer 第三方代理模块
author: xweiba
homepage: https://github.com/xweiba/location-spoofer
category: Tools
http:
mitm:
- "gs-loc.apple.com"
- "gs-loc-cn.apple.com"
- "gsp-ssl.ls.apple.com"
- "bluedot.is.autonavi.com"
- "bluedot.is.autonavi.com.gds.alibabadns.com"
script:
- match: ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc
name: WLOC.Location
type: response
require-body: true
binary-mode: true
max-size: 0
timeout: 30
argument: longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
- match: ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)
name: WLOC.Settings
type: request
require-body: false
timeout: 10
script-providers:
WLOC.Location:
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
interval: 86400
WLOC.Settings:
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
interval: 86400
+508
View File
@@ -0,0 +1,508 @@
{
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"dependencies": {
"pako": "2.1.0"
},
"devDependencies": {
"esbuild": "0.25.8"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.8",
"@esbuild/android-arm": "0.25.8",
"@esbuild/android-arm64": "0.25.8",
"@esbuild/android-x64": "0.25.8",
"@esbuild/darwin-arm64": "0.25.8",
"@esbuild/darwin-x64": "0.25.8",
"@esbuild/freebsd-arm64": "0.25.8",
"@esbuild/freebsd-x64": "0.25.8",
"@esbuild/linux-arm": "0.25.8",
"@esbuild/linux-arm64": "0.25.8",
"@esbuild/linux-ia32": "0.25.8",
"@esbuild/linux-loong64": "0.25.8",
"@esbuild/linux-mips64el": "0.25.8",
"@esbuild/linux-ppc64": "0.25.8",
"@esbuild/linux-riscv64": "0.25.8",
"@esbuild/linux-s390x": "0.25.8",
"@esbuild/linux-x64": "0.25.8",
"@esbuild/netbsd-arm64": "0.25.8",
"@esbuild/netbsd-x64": "0.25.8",
"@esbuild/openbsd-arm64": "0.25.8",
"@esbuild/openbsd-x64": "0.25.8",
"@esbuild/openharmony-arm64": "0.25.8",
"@esbuild/sunos-x64": "0.25.8",
"@esbuild/win32-arm64": "0.25.8",
"@esbuild/win32-ia32": "0.25.8",
"@esbuild/win32-x64": "0.25.8"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node build.mjs",
"test": "node --test"
},
"dependencies": {
"pako": "2.1.0"
},
"devDependencies": {
"esbuild": "0.25.8"
}
}
+263
View File
@@ -0,0 +1,263 @@
export const MOTION_ACTIVITY_TYPE = 63;
export const MOTION_ACTIVITY_CONFIDENCE = 467;
export const WLOC_MARKER = Uint8Array.from([0, 0, 0, 1, 0, 0]);
const UINT32_RANGE = 0x100000000;
const concat = (...parts) => {
const length = parts.reduce((sum, part) => sum + part.length, 0);
const out = new Uint8Array(length);
let offset = 0;
for (const part of parts) {
out.set(part, offset);
offset += part.length;
}
return out;
};
const equal = (left, right) =>
left.length === right.length && left.every((value, index) => value === right[index]);
function readVarint(data, offset) {
let value = 0;
let multiplier = 1;
for (let index = 0; index < 10 && offset + index < data.length; index += 1) {
const byte = data[offset + index];
const chunk = byte & 0x7f;
if (value !== null && chunk <= Math.floor((Number.MAX_SAFE_INTEGER - value) / multiplier)) {
value += chunk * multiplier;
} else {
value = null;
}
if ((byte & 0x80) === 0) return { value, next: offset + index + 1 };
multiplier *= 0x80;
}
throw new Error("invalid varint");
}
function writeVarint(input) {
const value = Math.trunc(Number(input));
if (!Number.isSafeInteger(value)) throw new Error("varint value is not a safe integer");
let low = value >>> 0;
let high = Math.floor(value / UINT32_RANGE) >>> 0;
const bytes = [];
do {
const byte = low & 0x7f;
low = ((low >>> 7) | ((high & 0x7f) << 25)) >>> 0;
high >>>= 7;
const hasMore = high !== 0 || low !== 0;
bytes.push(byte | (hasMore ? 0x80 : 0));
} while (high !== 0 || low !== 0);
return Uint8Array.from(bytes);
}
const writeTag = (number, wireType) => writeVarint((number << 3) | wireType);
function writeLengthDelimited(number, value) {
return concat(writeTag(number, 2), writeVarint(value.length), value);
}
export function parseFields(data) {
const fields = [];
let offset = 0;
while (offset < data.length) {
const start = offset;
const tag = readVarint(data, offset);
offset = tag.next;
if (!Number.isSafeInteger(tag.value)) throw new Error("protobuf tag is too large");
const number = Math.floor(tag.value / 8);
const wireType = tag.value & 7;
if (number === 0) throw new Error("invalid protobuf field 0");
let value;
if (wireType === 0) {
const item = readVarint(data, offset);
value = data.slice(offset, item.next);
offset = item.next;
} else if (wireType === 1) {
if (offset + 8 > data.length) throw new Error("truncated fixed64");
value = data.slice(offset, offset + 8);
offset += 8;
} else if (wireType === 2) {
const length = readVarint(data, offset);
offset = length.next;
const size = length.value;
if (!Number.isSafeInteger(size) || offset + size > data.length) {
throw new Error("truncated length-delimited field");
}
value = data.slice(offset, offset + size);
offset += size;
} else if (wireType === 5) {
if (offset + 4 > data.length) throw new Error("truncated fixed32");
value = data.slice(offset, offset + 4);
offset += 4;
} else {
throw new Error(`unsupported wire type ${wireType}`);
}
fields.push({ number, wireType, value, raw: data.slice(start, offset) });
}
return fields;
}
function patchLocation(data, config, stats) {
const fields = parseFields(data);
if (!fields.some((field) => field.number === 1 && field.wireType === 0) ||
!fields.some((field) => field.number === 2 && field.wireType === 0)) {
return data;
}
let hasMotionType = false;
let hasMotionConfidence = false;
const parts = fields.map((field) => {
if (field.wireType !== 0) return field.raw;
if (field.number === 1) return concat(writeTag(1, 0), writeVarint(Math.round(config.latitude * 1e8)));
if (field.number === 2) return concat(writeTag(2, 0), writeVarint(Math.round(config.longitude * 1e8)));
if (field.number === 3) return concat(writeTag(3, 0), writeVarint(config.accuracy));
if (config.motionSimulationEnabled && field.number === 11) {
hasMotionType = true;
return concat(writeTag(11, 0), writeVarint(MOTION_ACTIVITY_TYPE));
}
if (config.motionSimulationEnabled && field.number === 12) {
hasMotionConfidence = true;
return concat(writeTag(12, 0), writeVarint(MOTION_ACTIVITY_CONFIDENCE));
}
return field.raw;
});
if (config.motionSimulationEnabled && !hasMotionType) {
parts.push(concat(writeTag(11, 0), writeVarint(MOTION_ACTIVITY_TYPE)));
}
if (config.motionSimulationEnabled && !hasMotionConfidence) {
parts.push(concat(writeTag(12, 0), writeVarint(MOTION_ACTIVITY_CONFIDENCE)));
}
const out = concat(...parts);
if (!equal(out, data)) stats.locations += 1;
return out;
}
function patchWifi(data, config, stats) {
const fields = parseFields(data);
const hasMac = fields.some((field) =>
field.number === 1 && field.wireType === 2 &&
/^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$/.test(
Array.from(field.value, (byte) => String.fromCharCode(byte)).join("")
)
);
if (!hasMac) return data;
let changed = false;
const parts = fields.map((field) => {
if (field.number !== 2 || field.wireType !== 2) return field.raw;
const value = patchLocation(field.value, config, stats);
changed ||= !equal(value, field.value);
return writeLengthDelimited(2, value);
});
if (changed) stats.wifi += 1;
return concat(...parts);
}
function patchCell(data, config, stats) {
let changed = false;
const parts = parseFields(data).map((field) => {
if (field.number !== 5 || field.wireType !== 2) return field.raw;
const value = patchLocation(field.value, config, stats);
changed ||= !equal(value, field.value);
return writeLengthDelimited(5, value);
});
if (changed) stats.cell += 1;
return concat(...parts);
}
export function patchPayload(data, config, stats = { wifi: 0, cell: 0, locations: 0 }) {
const parts = parseFields(data).map((field) => {
if (field.number === 2 && field.wireType === 2) {
return writeLengthDelimited(2, patchWifi(field.value, config, stats));
}
if ((field.number === 22 || field.number === 24) && field.wireType === 2) {
return writeLengthDelimited(field.number, patchCell(field.value, config, stats));
}
return field.raw;
});
return { data: concat(...parts), stats };
}
const uint16 = (data, offset) => (data[offset] << 8) | data[offset + 1];
const uint32 = (data, offset) =>
((data[offset] * 0x1000000) + (data[offset + 1] << 16) +
(data[offset + 2] << 8) + data[offset + 3]) >>> 0;
const writeUint16 = (value) => Uint8Array.from([(value >>> 8) & 0xff, value & 0xff]);
const writeUint32 = (value) => Uint8Array.from([
(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff
]);
function findBytes(data, marker) {
outer: for (let offset = 0; offset <= data.length - marker.length; offset += 1) {
for (let index = 0; index < marker.length; index += 1) {
if (data[offset + index] !== marker[index]) continue outer;
}
return offset;
}
return -1;
}
function patchARPC(body, config) {
if (body.length < 2) throw new Error("short ARPC");
let offset = 2;
for (let index = 0; index < 3; index += 1) {
if (offset + 2 > body.length) throw new Error("truncated ARPC string");
offset += 2 + uint16(body, offset);
}
const lengthOffset = offset + 4;
const payloadOffset = lengthOffset + 4;
if (payloadOffset > body.length) throw new Error("truncated ARPC header");
const length = uint32(body, lengthOffset);
if (!length || payloadOffset + length > body.length) throw new Error("invalid ARPC length");
const patched = patchPayload(body.slice(payloadOffset, payloadOffset + length), config);
if (equal(patched.data, body.slice(payloadOffset, payloadOffset + length))) throw new Error("unchanged ARPC");
return { data: concat(body.slice(0, lengthOffset), writeUint32(patched.data.length),
patched.data, body.slice(payloadOffset + length)), stats: patched.stats };
}
function patchMarker(body, config) {
const markerOffset = findBytes(body, WLOC_MARKER);
if (markerOffset < 0) throw new Error("marker not found");
const lengthOffset = markerOffset + WLOC_MARKER.length;
const payloadOffset = lengthOffset + 2;
const length = uint16(body, lengthOffset);
if (!length || payloadOffset + length > body.length) throw new Error("invalid marker length");
const patched = patchPayload(body.slice(payloadOffset, payloadOffset + length), config);
if (patched.data.length > 65535 || equal(patched.data, body.slice(payloadOffset, payloadOffset + length))) {
throw new Error("unchanged marker");
}
return { data: concat(body.slice(0, lengthOffset), writeUint16(patched.data.length),
patched.data, body.slice(payloadOffset + length)), stats: patched.stats };
}
function patchSynthetic(body, offset, config) {
if (offset + 10 > body.length) throw new Error("short frame");
const length = uint16(body, offset + 8);
if (!length || offset + 10 + length > body.length) throw new Error("invalid frame");
const patched = patchPayload(body.slice(offset + 10, offset + 10 + length), config);
if (patched.data.length > 65535 || equal(patched.data, body.slice(offset + 10, offset + 10 + length))) {
throw new Error("unchanged frame");
}
return { data: concat(body.slice(0, offset + 8), writeUint16(patched.data.length),
patched.data, body.slice(offset + 10 + length)), stats: patched.stats };
}
export function patchWlocBody(body, config) {
for (const patcher of [patchARPC, patchMarker]) {
try { return patcher(body, config); } catch {}
}
const offsets = [...new Set([0, 2, 4, 6, 8, 10, 12, 14, 16,
...Array.from({ length: Math.min(96, Math.max(0, body.length - 10)) + 1 }, (_, index) => index)])];
for (const offset of offsets) {
try { return patchSynthetic(body, offset, config); } catch {}
}
for (let offset = 0; offset <= Math.min(256, body.length); offset += 1) {
try {
const patched = patchPayload(body.slice(offset), config);
if (!equal(patched.data, body.slice(offset))) {
return { data: concat(body.slice(0, offset), patched.data), stats: patched.stats };
}
} catch {}
}
throw new Error("no patchable wloc payload found");
}
export const internals = { concat, writeVarint, writeTag, writeLengthDelimited, equal };
+26
View File
@@ -0,0 +1,26 @@
import { ungzip } from "pako";
import { patchWlocBody } from "./core.js";
import {
STORAGE_KEY, finishBinary, finishPassthrough, readPersistent, responseBytes
} from "./runtime.js";
try {
const settings = readPersistent(STORAGE_KEY);
const input = responseBytes();
if (!settings || !settings.enabled || !input.length) {
finishPassthrough();
} else {
const isGzip = input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b;
const body = isGzip ? ungzip(input) : input;
const patched = patchWlocBody(body, {
latitude: Number(settings.latitude),
longitude: Number(settings.longitude),
accuracy: Number(settings.accuracy != null ? settings.accuracy : 25),
motionSimulationEnabled: settings.motionSimulationEnabled === true
});
finishBinary(patched.data);
}
} catch (error) {
console.log(`[Location Spoofer] ${error && error.message ? error.message : error}`);
finishPassthrough();
}
+109
View File
@@ -0,0 +1,109 @@
export const MODULE_VERSION = "1.0.0";
export const PROTOCOL_VERSION = 1;
export const CAPABILITIES = [
"wifi", "cellTower", "arpc", "marker", "synthetic", "bare", "motionSimulation"
];
export const STORAGE_KEY = "locationSpoofer.settings.v1";
export function environment() {
if (typeof $task !== "undefined") return "quantumultX";
if (typeof $loon !== "undefined") return "loon";
if (typeof $rocket !== "undefined") return "shadowrocket";
if (typeof Egern !== "undefined") return "egern";
if (typeof $environment !== "undefined" && $environment["stash-version"]) return "stash";
if (typeof $environment !== "undefined" && $environment["surge-version"]) return "surge";
return "unknown";
}
export function readPersistent(key) {
const raw = environment() === "quantumultX"
? $prefs.valueForKey(key)
: $persistentStore.read(key);
if (!raw) return null;
try { return JSON.parse(raw); } catch { return null; }
}
export function writePersistent(key, value) {
const raw = value == null ? "" : JSON.stringify(value);
return environment() === "quantumultX"
? $prefs.setValueForKey(raw, key)
: $persistentStore.write(raw, key);
}
export function responseBytes() {
const response = typeof $response === "undefined" ? null : $response;
const value = response && response.bodyBytes != null ? response.bodyBytes : response && response.body;
if (value instanceof ArrayBuffer) return new Uint8Array(value);
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
if (typeof value === "string") {
return Uint8Array.from(value, (character) => character.charCodeAt(0) & 0xff);
}
return new Uint8Array();
}
function cleanHeaders(headers, length) {
const out = Object.assign({}, headers || {});
for (const name of ["Content-Encoding", "content-encoding", "Transfer-Encoding", "transfer-encoding"]) {
delete out[name];
}
out["Content-Length"] = String(length);
return out;
}
export function finishBinary(bytes) {
const response = typeof $response === "undefined" ? {} : $response;
const headers = cleanHeaders(response.headers, bytes.length);
const env = environment();
if (env === "quantumultX") {
delete headers["Content-Length"];
$done({ status: "HTTP/1.1 200 OK", headers, bodyBytes: bytes.buffer });
} else if (env === "stash") {
$done(Object.assign({}, response, { status: 200, headers, body: bytes }));
} else {
$done({ response: Object.assign({}, response, { status: 200, headers, body: bytes }) });
}
}
export function finishPassthrough() {
$done({});
}
export function finishJSON(value) {
const response = {
status: 200,
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(value)
};
if (environment() === "quantumultX") {
$done(Object.assign({}, response, { status: "HTTP/1.1 200 OK" }));
} else if (environment() === "stash") {
$done(response);
} else {
$done({ response });
}
}
export function queryParameters(url) {
const query = url.split("?")[1] || "";
const values = {};
query.split("&").forEach((item) => {
if (!item) return;
const separator = item.indexOf("=");
const rawKey = separator < 0 ? item : item.slice(0, separator);
const rawValue = separator < 0 ? "" : item.slice(separator + 1);
let key = rawKey;
let value = rawValue;
try { key = decodeURIComponent(rawKey.replace(/\+/g, " ")); } catch {}
try { value = decodeURIComponent(rawValue.replace(/\+/g, " ")); } catch {}
if (!Object.prototype.hasOwnProperty.call(values, key)) values[key] = value;
});
return values;
}
export function requestPath(url) {
const withoutQuery = url.split("?")[0];
const scheme = withoutQuery.indexOf("://");
if (scheme < 0) return withoutQuery;
const path = withoutQuery.indexOf("/", scheme + 3);
return path < 0 ? "/" : withoutQuery.slice(path);
}
+48
View File
@@ -0,0 +1,48 @@
import {
CAPABILITIES, MODULE_VERSION, PROTOCOL_VERSION, STORAGE_KEY,
finishJSON, queryParameters, readPersistent, requestPath, writePersistent
} from "./runtime.js";
const url = typeof $request === "undefined" ? "" : ($request.url || "");
const path = requestPath(url);
const parameters = queryParameters(url);
if (path === "/wloc-settings/version") {
finishJSON({
success: true,
moduleVersion: MODULE_VERSION,
protocolVersion: PROTOCOL_VERSION,
capabilities: CAPABILITIES
});
} else if (parameters.action === "query") {
const settings = readPersistent(STORAGE_KEY);
finishJSON(settings && settings.enabled
? { success: true, longitude: settings.longitude, latitude: settings.latitude,
accuracy: settings.accuracy, motionSimulationEnabled: settings.motionSimulationEnabled === true }
: { success: false, error: "无已保存的坐标" });
} else if (parameters.action === "clear") {
writePersistent(STORAGE_KEY, null);
finishJSON({ success: true });
} else {
const longitude = Number(parameters.lon != null ? parameters.lon : parameters.longitude);
const latitude = Number(parameters.lat != null ? parameters.lat : parameters.latitude);
const accuracy = Number(parameters.acc != null
? parameters.acc
: (parameters.accuracy != null ? parameters.accuracy : 25));
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
finishJSON({ success: false, error: "缺少 lon/lat 参数" });
} else {
const settings = {
enabled: true,
longitude,
latitude,
accuracy,
motionSimulationEnabled: parameters.motion === "1"
};
const success = writePersistent(STORAGE_KEY, settings);
finishJSON(success
? { success: true, longitude, latitude, accuracy,
motionSimulationEnabled: settings.motionSimulationEnabled }
: { success: false, error: "保存配置失败" });
}
}
@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import vm from "node:vm";
const bundles = [
new URL("../dist/v1/wloc.js", import.meta.url),
new URL("../dist/v1/wloc-settings.js", import.meta.url)
];
test("generated bundles avoid newer JavaScriptCore runtime requirements", async () => {
for (const bundle of bundles) {
const source = await readFile(bundle, "utf8");
for (const unsupported of [
/\bBigInt\b/,
/\bglobalThis\b/,
/\bURLSearchParams\b/,
/\bObject\.fromEntries\b/,
/\?\./,
/\?\?/
]) {
assert.equal(unsupported.test(source), false, `${bundle.pathname} contains ${unsupported}`);
}
}
});
test("settings bundle runs without modern URL and text globals", async () => {
const source = await readFile(bundles[1], "utf8");
let result;
const storage = new Map();
vm.runInNewContext(source, {
$rocket: {},
$request: {
url: "https://gs-loc.apple.com/wloc-settings/save?lon=121.1&lat=31.2&acc=25"
},
$persistentStore: {
read: (key) => storage.get(key) || null,
write: (value, key) => {
storage.set(key, value);
return true;
}
},
$done: (value) => { result = value; },
JSON,
Number,
Object,
String,
decodeURIComponent
});
assert.equal(JSON.parse(result.response.body).success, true);
});
+85
View File
@@ -0,0 +1,85 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
MOTION_ACTIVITY_CONFIDENCE, MOTION_ACTIVITY_TYPE, internals, parseFields, patchWlocBody
} from "../src/core.js";
const { concat, writeVarint, writeTag, writeLengthDelimited } = internals;
function location(withMotion = false) {
const fields = [
concat(writeTag(1, 0), writeVarint(100)),
concat(writeTag(2, 0), writeVarint(200)),
concat(writeTag(3, 0), writeVarint(25))
];
if (withMotion) {
fields.push(concat(writeTag(11, 0), writeVarint(7)));
fields.push(concat(writeTag(12, 0), writeVarint(88)));
}
return concat(...fields);
}
function wifiPayload(value = location()) {
const device = concat(
writeLengthDelimited(1, new TextEncoder().encode("aa:bb:cc:dd:ee:ff")),
writeLengthDelimited(2, value)
);
return writeLengthDelimited(2, device);
}
function frame(payload) {
return concat(Uint8Array.from([0, 1, 0, 0, 0, 1, 0, 0]),
Uint8Array.from([payload.length >> 8, payload.length & 0xff]), payload);
}
const config = {
latitude: 31.230416,
longitude: 121.473701,
accuracy: 50,
motionSimulationEnabled: false
};
test("patches synthetic Wi-Fi response", () => {
const result = patchWlocBody(frame(wifiPayload()), config);
assert.equal(result.stats.wifi, 1);
assert.equal(result.stats.locations, 1);
});
test("preserves motion fields while disabled", () => {
const result = patchWlocBody(frame(wifiPayload(location(true))), config);
const payload = result.data.slice(10);
assert.ok(payload.includes(7));
assert.ok(payload.includes(88));
});
test("replaces motion fields while enabled", () => {
const result = patchWlocBody(frame(wifiPayload(location(true))), {
...config, motionSimulationEnabled: true
});
const root = parseFields(result.data.slice(10));
const device = parseFields(root[0].value);
const fields = parseFields(device.find((field) => field.number === 2).value);
const motionType = fields.find((field) => field.number === 11);
const motionConfidence = fields.find((field) => field.number === 12);
assert.deepEqual(motionType.value, writeVarint(MOTION_ACTIVITY_TYPE));
assert.deepEqual(motionConfidence.value, writeVarint(MOTION_ACTIVITY_CONFIDENCE));
});
test("patches CellTower fields 22 and 24", () => {
for (const number of [22, 24]) {
const cell = writeLengthDelimited(5, location());
const result = patchWlocBody(frame(writeLengthDelimited(number, cell)), config);
assert.equal(result.stats.cell, 1);
}
});
test("encodes signed int64 coordinates without BigInt", () => {
assert.deepEqual(
Array.from(writeVarint(-18_000_000_000)),
[128, 152, 247, 248, 188, 255, 255, 255, 255, 1]
);
assert.deepEqual(
Array.from(writeVarint(18_000_000_000)),
[128, 232, 136, 135, 67]
);
});
+46 -21
View File
@@ -1,20 +1,26 @@
# Third-party proxy modules # Third-party proxy modules
Third-party proxy mode (Surge / Quantumult X / Loon / Shadowrocket / Stash / The third-party proxy modules and WLOC scripts are maintained in this
Egern) now relies entirely on the upstream repository. The original `Yu9191/wloc` repository was deleted, and its Raw
[Yu9191/wloc](https://github.com/Yu9191/wloc) modules. The App no longer URLs now return HTTP 404, so it is retained only as provenance and is not a
maintains or ships project-owned module/script copies, so it never drifts runtime dependency.
from the upstream protocol.
## Subscription addresses ## Hosted files
The App builds each client's module subscription URL directly from the - Mirrored module subscriptions: `Resources/ThirdPartyProxyModules/`
upstream repository: - Direct module subscriptions: `ThirdParty/WlocScripts/modules/direct/`
- Versioned generated scripts: `ThirdParty/WlocScripts/dist/v1/`
- Script source and tests: `ThirdParty/WlocScripts/src/` and
`ThirdParty/WlocScripts/test/`
- default mirror (gh-proxy): The App appends `?v=1.0.7` to module subscription URLs, and every module uses
`https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>` the same query parameter for its generated script URLs, to invalidate client
caches:
- default mirror:
`https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/<file>?v=1.0.7`
- direct: - direct:
`https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>` `https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/modules/direct/<file>?v=1.0.7`
| Module file | Client | | Module file | Client |
|---|---| |---|---|
@@ -24,17 +30,36 @@ upstream repository:
| `wloc.lpx` | Loon | | `wloc.lpx` | Loon |
| `wloc.stoverride` | Stash | | `wloc.stoverride` | Stash |
No `?v=` cache-bust is appended: the URL points at upstream's latest content, The hosted files become downloadable from these URLs after this change is
and re-importing the subscription in the proxy client re-fetches it. merged into `main`. Until then, validate them from the checked-out paths.
## Script protocol ## Script protocol
The upstream `wloc.js` patches Apple WLOC responses and reads coordinates from `wloc.js` patches Apple WLOC responses and reads coordinates from the
the `wloc_settings` persistent key or the module `argument` config. The `wloc_settings` persistent key or the module `argument` config.
upstream `wloc-settings.js` implements `wloc-settings/save` (query/clear/save) `wloc-settings.js` implements `wloc-settings/save` (query/clear/save) using
using `lon`/`lat`/`acc`/`randomRadius` parameters. `lon`/`lat`/`acc`/`randomRadius` parameters.
The App's third-party save sends `lon`/`lat`/`acc`, matching the upstream The App's third-party save sends `lon`/`lat`/`acc`. Motion-state simulation
script. Motion-state simulation (fields 11/12) is **not** implemented by the (fields 11/12) is unavailable in third-party mode; it remains available in
upstream scripts and is unavailable in third-party mode; it remains available App Mode through the built-in proxy.
in APP mode (built-in proxy).
## Provenance and maintenance
The response-rewrite approach and original module structure were derived from
`Yu9191/wloc`. The repository is no longer available, but its attribution is
preserved in module metadata and project acknowledgements. Bundled dependency
notices are recorded in `ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md`.
Build and test the scripts with:
```bash
cd ThirdParty/WlocScripts
npm ci
npm test
npm run build
```
If an authoritative, maintained upstream returns, compare its license,
protocol compatibility, tests, and release guarantees before considering a
switch away from the project-owned copies.