mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
fix(i18n): 完善国际化并迁移 WLOC 脚本
This commit is contained in:
+32
-18
@@ -95,9 +95,10 @@ struct BugReportView: View {
|
||||
do {
|
||||
let response = try await thirdPartyProxy.query()
|
||||
let active = response.success && response.latitude != nil && response.longitude != nil
|
||||
testLog = "第三方代理测试模式:模块连接成功;已保存坐标=\(active ? "是" : "否")"
|
||||
let savedCoordinate = active ? String(localized: "是") : String(localized: "否")
|
||||
testLog = String(localized: "第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
|
||||
} catch {
|
||||
testLog = "第三方代理测试模式:模块连接失败;\(error.localizedDescription)"
|
||||
testLog = String(localized: "第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
_ = await setup.runVerificationTest()
|
||||
@@ -113,22 +114,11 @@ struct BugReportView: View {
|
||||
let systemVersion = UIDevice.current.systemVersion
|
||||
|
||||
// 拼接报告
|
||||
let report = """
|
||||
### 环境信息
|
||||
App 版本: \(appVersion)
|
||||
系统版本: iOS \(systemVersion)
|
||||
运行模式: \(runtimeMode.mode.displayName)
|
||||
第三方客户端: \(runtimeMode.mode == .thirdParty ? thirdPartyClient.selectedClient.name : "不适用")
|
||||
可复现环境: \(isReproducible ? "是" : "否")
|
||||
|
||||
### 问题描述
|
||||
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
|
||||
### 诊断日志
|
||||
```
|
||||
\(testLog.isEmpty ? "(无诊断数据)" : testLog)
|
||||
```
|
||||
"""
|
||||
let report = bugReport(
|
||||
appVersion: appVersion,
|
||||
systemVersion: systemVersion,
|
||||
testLog: testLog
|
||||
)
|
||||
|
||||
// 复制到剪切板
|
||||
UIPasteboard.general.string = report
|
||||
@@ -138,4 +128,28 @@ struct BugReportView: View {
|
||||
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
@@ -23,7 +23,10 @@ struct RuntimeLogsView: View {
|
||||
private var filteredEntries: [RuntimeLogEntry] {
|
||||
let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
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 {
|
||||
@@ -201,7 +204,7 @@ struct RuntimeLogsView: View {
|
||||
.foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("\(entry.source) \(entry.category)").font(.caption.weight(.semibold))
|
||||
Text("\(entry.source) \(entry.localizedCategory)").font(.caption.weight(.semibold))
|
||||
Spacer()
|
||||
Button {
|
||||
UIPasteboard.general.string = entry.renderedText
|
||||
@@ -225,9 +228,9 @@ struct RuntimeLogsView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Text(entry.message).font(.caption.monospaced()).textSelection(.enabled)
|
||||
Text(entry.localizedMessage).font(.caption.monospaced()).textSelection(.enabled)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -246,22 +249,26 @@ struct RuntimeLogsView: View {
|
||||
let active = response.success && response.latitude != nil && response.longitude != nil
|
||||
testSucceeded = true
|
||||
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
|
||||
testMessage = """
|
||||
======== 第三方代理连接检测 ========
|
||||
模式: 测试模式
|
||||
请求: wloc-settings/save?action=query
|
||||
拦截响应: 有效 JSON
|
||||
已保存坐标: \(active ? "是" : "否")
|
||||
"""
|
||||
testMessage = thirdPartyTestLog(active: active)
|
||||
} catch {
|
||||
testSucceeded = false
|
||||
testResult = String(localized: "第三方模块连接失败")
|
||||
testMessage = """
|
||||
======== 第三方代理连接检测 ========
|
||||
模式: 测试模式
|
||||
请求: wloc-settings/save?action=query
|
||||
结果: \(error.localizedDescription)
|
||||
"""
|
||||
testMessage = thirdPartyTestLog(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -220,15 +220,15 @@ struct FirstSetupView: View {
|
||||
return thirdPartyTestFailure.message
|
||||
}
|
||||
guard !setup.message.isEmpty else { return nil }
|
||||
return """
|
||||
======== 第三方代理运行检测 ========
|
||||
当前客户端:\(thirdPartyClient.selectedClient.name)
|
||||
触发来源:地图或设置中的第三方代理操作
|
||||
请求动作:WLOC 配置接口
|
||||
检测结果:失败
|
||||
错误详情:\(setup.message)
|
||||
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。
|
||||
"""
|
||||
return [
|
||||
String(localized: "======== 第三方代理运行检测 ========"),
|
||||
String(localized: "当前客户端:\(thirdPartyClient.selectedClient.name)"),
|
||||
String(localized: "触发来源:地图或设置中的第三方代理操作"),
|
||||
String(localized: "请求动作:WLOC 配置接口"),
|
||||
String(localized: "检测结果:失败"),
|
||||
String(localized: "错误详情:\(setup.message)"),
|
||||
String(localized: "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
|
||||
].joined(separator: "\n")
|
||||
}
|
||||
|
||||
private var modeStep: some View {
|
||||
@@ -468,10 +468,10 @@ struct FirstSetupView: View {
|
||||
|
||||
Text("返回格式")
|
||||
.font(.subheadline.bold())
|
||||
Text("""
|
||||
Text(String(localized: """
|
||||
成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25}
|
||||
失败:{"success":false,"error":"错误说明"}
|
||||
""")
|
||||
"""))
|
||||
.font(.caption.monospaced())
|
||||
.textSelection(.enabled)
|
||||
|
||||
@@ -641,7 +641,7 @@ struct FirstSetupView: View {
|
||||
private func setupScreenshot(
|
||||
assetName: String,
|
||||
title: String,
|
||||
caption: LocalizedStringKey
|
||||
caption: String
|
||||
) -> some View {
|
||||
if let image = UIImage(named: assetName) {
|
||||
Button {
|
||||
@@ -657,7 +657,7 @@ struct FirstSetupView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||
Text(caption)
|
||||
Text(LocalizedStringKey(caption))
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -672,7 +672,11 @@ struct FirstSetupView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(LocalizedStringKey(title))
|
||||
.accessibilityLabel(
|
||||
Text(LocalizedStringKey(title))
|
||||
+ Text(": ")
|
||||
+ Text(LocalizedStringKey(caption))
|
||||
)
|
||||
.accessibilityHint("轻点查看大图")
|
||||
}
|
||||
}
|
||||
@@ -872,21 +876,19 @@ struct FirstSetupView: View {
|
||||
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
|
||||
]
|
||||
)
|
||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(
|
||||
message: """
|
||||
======== 第三方代理连接检测 ========
|
||||
当前客户端:\(client.name)
|
||||
配置接口:/wloc-settings/save
|
||||
请求动作:WLOC query
|
||||
检查范围:模块拦截、MITM、证书、代理/VPN 连接
|
||||
连接状态:\(connectionState)
|
||||
检测结果:失败
|
||||
耗时:\(elapsedMilliseconds) ms
|
||||
错误类型:\(errorType)
|
||||
错误详情:\(error.localizedDescription)
|
||||
处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。
|
||||
"""
|
||||
)
|
||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
|
||||
String(localized: "======== 第三方代理连接检测 ========"),
|
||||
String(localized: "当前客户端:\(client.name)"),
|
||||
String(localized: "配置接口:/wloc-settings/save"),
|
||||
String(localized: "请求动作:WLOC query"),
|
||||
String(localized: "检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
|
||||
String(localized: "连接状态:\(connectionState)"),
|
||||
String(localized: "检测结果:失败"),
|
||||
String(localized: "耗时:\(elapsedMilliseconds) ms"),
|
||||
String(localized: "错误类型:\(errorType)"),
|
||||
String(localized: "错误详情:\(error.localizedDescription)"),
|
||||
String(localized: "处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。")
|
||||
].joined(separator: "\n"))
|
||||
showsThirdPartyFailureLog = true
|
||||
}
|
||||
}
|
||||
@@ -895,11 +897,11 @@ struct FirstSetupView: View {
|
||||
private var thirdPartyConnectionStateDescription: String {
|
||||
switch thirdPartyProxy.connectionState {
|
||||
case .unknown:
|
||||
return "未检测"
|
||||
return String(localized: "未检测")
|
||||
case .connected(let active):
|
||||
return active ? "已连接,有保存坐标" : "已连接,无保存坐标"
|
||||
return active ? String(localized: "已连接,有保存坐标") : String(localized: "已连接,无保存坐标")
|
||||
case .failed(let message):
|
||||
return "连接失败(\(message))"
|
||||
return String(localized: "连接失败(\(message))")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -766,7 +766,8 @@ struct MapHomeView: View {
|
||||
private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) {
|
||||
let count = tipPreferences.recordSuccessfulOperation(kind)
|
||||
let operationName = kind == .activation ? "开启" : "关闭"
|
||||
RuntimeLogger.info("APP", "提醒", "累计\(operationName)虚拟定位次数", details: [
|
||||
RuntimeLogger.info("APP", "提醒", "累计虚拟定位操作次数", details: [
|
||||
"操作": operationName,
|
||||
"次数": String(count),
|
||||
"运行模式": runtimeMode.mode.displayName,
|
||||
"可显示不再提醒": String(tipPreferences.canSuppress(kind))
|
||||
@@ -1335,7 +1336,9 @@ struct MapHomeView: View {
|
||||
context.showFailureAlert,
|
||||
!Task.isCancelled,
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable {
|
||||
|
||||
enum ProxyError: LocalizedError {
|
||||
case startFailed
|
||||
var errorDescription: String? { "Go proxy 启动失败" }
|
||||
var errorDescription: String? { String(localized: "Go proxy 启动失败") }
|
||||
}
|
||||
|
||||
@@ -222,11 +222,11 @@ struct SettingsView: View {
|
||||
|
||||
Section("致谢") {
|
||||
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)
|
||||
}
|
||||
} label: {
|
||||
Label("核心定位改写逻辑移植自 Yu9191/wloc", systemImage: "heart.fill")
|
||||
Label("定位改写脚本源自 Yu9191/wloc,现由本项目维护", systemImage: "heart.fill")
|
||||
.foregroundStyle(.pink)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -108,36 +108,36 @@ final class SetupCoordinator: ObservableObject {
|
||||
testLog = ""
|
||||
let log = { (msg: String) in self.testLog += msg + "\n" }
|
||||
|
||||
log("======== 代理验证测试 ========")
|
||||
log("App 版本: \(appVersion)")
|
||||
log("系统版本: iOS \(UIDevice.current.systemVersion)")
|
||||
log(String(localized: "======== 代理验证测试 ========"))
|
||||
log(String(localized: "App 版本: \(appVersion)"))
|
||||
log(String(localized: "系统版本: iOS \(UIDevice.current.systemVersion)"))
|
||||
log("")
|
||||
|
||||
// Step A: Proxy running
|
||||
log("[步骤 A] 检查代理是否运行…")
|
||||
log(" 端口: 127.0.0.1:8888")
|
||||
log(String(localized: "[步骤 A] 检查代理是否运行…"))
|
||||
log(String(localized: " 端口: 127.0.0.1:8888"))
|
||||
let stepAStart = Date()
|
||||
if !proxy.isRunning {
|
||||
log(" ⚠ 代理未运行,尝试启动…")
|
||||
log(String(localized: " ⚠ 代理未运行,尝试启动…"))
|
||||
do { try await proxy.start() } catch {
|
||||
log(" ✗ 启动失败: \(error.localizedDescription)")
|
||||
log(String(localized: " ✗ 启动失败: \(error.localizedDescription)"))
|
||||
return .proxyNotRunning
|
||||
}
|
||||
log(" ✓ 代理启动成功")
|
||||
log(String(localized: " ✓ 代理启动成功"))
|
||||
} else {
|
||||
log(" ✓ 代理已在运行中")
|
||||
log(String(localized: " ✓ 代理已在运行中"))
|
||||
}
|
||||
collectProxyLogs(since: stepAStart, to: log)
|
||||
|
||||
// Step B: Combined CA + WiFi proxy check (single request)
|
||||
log("")
|
||||
log("[步骤 B] 检测证书与 WiFi 代理…")
|
||||
log(" 方式: 请求 baidu.com/paopao-verify-<token>")
|
||||
log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过")
|
||||
log(String(localized: "[步骤 B] 检测证书与 WiFi 代理…"))
|
||||
log(String(localized: " 方式: 请求 baidu.com/paopao-verify-<token>"))
|
||||
log(String(localized: " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
|
||||
let stepBStart = Date()
|
||||
let verifyToken = CoreBridge.refreshVerifyToken()
|
||||
guard !verifyToken.isEmpty else {
|
||||
log(" ✗ 无法生成验证 token")
|
||||
log(String(localized: " ✗ 无法生成验证 token"))
|
||||
return .certNotTrusted
|
||||
}
|
||||
do {
|
||||
@@ -150,17 +150,17 @@ final class SetupCoordinator: ObservableObject {
|
||||
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
if body == verifyToken {
|
||||
log(" ✓ 证书已信任,WiFi 代理已配置")
|
||||
log(String(localized: " ✓ 证书已信任,WiFi 代理已配置"))
|
||||
} else {
|
||||
log(" ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置")
|
||||
log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置"))
|
||||
return .wifiProxyNotConfigured
|
||||
}
|
||||
} catch {
|
||||
let ns = error as NSError
|
||||
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) {
|
||||
log(" TLS/证书校验失败,CA 证书未信任")
|
||||
log(String(localized: " TLS/证书校验失败,CA 证书未信任"))
|
||||
return .certNotTrusted
|
||||
}
|
||||
return .wifiProxyNotConfigured
|
||||
@@ -168,7 +168,7 @@ final class SetupCoordinator: ObservableObject {
|
||||
collectProxyLogs(since: stepBStart, to: log)
|
||||
|
||||
log("")
|
||||
log("======== 环境检测通过 ✓ ========")
|
||||
log(String(localized: "======== 环境检测通过 ✓ ========"))
|
||||
return .success
|
||||
}
|
||||
|
||||
@@ -202,9 +202,9 @@ final class SetupCoordinator: ObservableObject {
|
||||
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
|
||||
}
|
||||
guard !proxyEntries.isEmpty else { return }
|
||||
log(" --- 代理日志 ---")
|
||||
log(String(localized: " --- 代理日志 ---"))
|
||||
for e in proxyEntries {
|
||||
log(" " + e.message)
|
||||
log(" " + e.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -229,10 +229,10 @@ Current client status:
|
||||
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
|
||||
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)
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
|
||||
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
|
||||
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;
|
||||
- Issue reports are copied by the user before being submitted to GitHub;
|
||||
- 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;
|
||||
- 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
|
||||
|
||||
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)
|
||||
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||
|
||||
@@ -219,10 +219,9 @@ App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管
|
||||
|
||||
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
|
||||
|
||||
相关模块快照和来源记录:
|
||||
模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖:
|
||||
|
||||
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
|
||||
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
|
||||
|
||||
@@ -351,7 +350,7 @@ dist/PaopaoLocationSpoofer-unsigned.ipa
|
||||
- 运行日志保存在设备 App Group 容器中,并自动保留近三天;
|
||||
- 问题报告需要用户主动复制后提交到 GitHub;
|
||||
- APP 模式会访问本机代理和环境验证地址;
|
||||
- 第三方代理模式可能访问上游模块地址和 WLOC 配置接口;
|
||||
- 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
|
||||
- App 生成的 CA 私钥保存在设备 Keychain 中;
|
||||
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
|
||||
|
||||
@@ -430,7 +429,7 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
|
||||
|
||||
## 致谢与友链
|
||||
|
||||
核心定位响应处理思路、Go 实现和第三方模块参考自:
|
||||
核心定位响应处理思路、Go 实现和第三方模块参考自(`Yu9191/wloc` 原仓库已删除):
|
||||
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
- [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
|
||||
@@ -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
|
||||
@@ -142,6 +142,12 @@
|
||||
"第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present";
|
||||
"第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet";
|
||||
"第三方模块连接失败" = "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 */
|
||||
"生效说明" = "How to Activate";
|
||||
@@ -186,7 +192,7 @@
|
||||
"去提交" = "Submit";
|
||||
"复制模板" = "Copy Template";
|
||||
"不再提示" = "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.";
|
||||
"已复制投稿模板" = "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.";
|
||||
@@ -330,6 +336,367 @@
|
||||
"第三方代理清除坐标失败" = "The third-party proxy failed to clear the coordinate";
|
||||
"已有第三方代理请求正在执行" = "A third-party proxy request is already running";
|
||||
"配置已提供,尚未验证" = "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 */
|
||||
"证书下载地址无效" = "Invalid certificate download URL";
|
||||
|
||||
@@ -13,8 +13,8 @@ enum CertificateAuthorityStoreError: LocalizedError {
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidAuthority: return "本地 CA 证书或私钥无效"
|
||||
case let .keychain(status): return "无法写入设备钥匙串(\(status))"
|
||||
case .invalidAuthority: return String(localized: "本地 CA 证书或私钥无效")
|
||||
case let .keychain(status): return String(localized: "无法写入设备钥匙串(\(status))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ enum CoreBridgeError: LocalizedError {
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .generationFailed: return "无法生成本地证书"
|
||||
case .serverStartFailed: return "无法启动本地证书服务"
|
||||
case .generationFailed: return String(localized: "无法生成本地证书")
|
||||
case .serverStartFailed: return String(localized: "无法启动本地证书服务")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -38,10 +38,35 @@ struct RuntimeLogEntry: Codable, Identifiable, Equatable {
|
||||
var renderedText: String {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
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: " ")
|
||||
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(category)] \(message)\(suffix)"
|
||||
: " " + localizedDetails.sorted(by: { $0.0 < $1.0 }).map { "\($0.0)=\($0.1)" }.joined(separator: " ")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,10 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
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: "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 }
|
||||
@@ -203,6 +206,9 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
}
|
||||
|
||||
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 surge
|
||||
case quantumultX
|
||||
@@ -239,21 +245,14 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
||||
|
||||
@MainActor
|
||||
var subscriptionURL: URL {
|
||||
// Third-party modules are served directly from the upstream Yu9191/wloc
|
||||
// repository (mirror via gh-proxy when enabled). We no longer maintain
|
||||
// project-owned module copies, so the URL tracks upstream releases.
|
||||
let fileName: String
|
||||
switch self {
|
||||
case .surge, .egern: fileName = "wloc.sgmodule"
|
||||
case .quantumultX: fileName = "wloc.conf"
|
||||
case .loon: fileName = "wloc.lpx"
|
||||
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)!
|
||||
let directory = ThirdPartyModuleSourceStore.shared.useMirror
|
||||
? "Resources/ThirdPartyProxyModules"
|
||||
: "ThirdParty/WlocScripts/modules/direct"
|
||||
let prefix = ThirdPartyModuleSourceStore.shared.useMirror
|
||||
? "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
|
||||
: "https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
|
||||
let base = "\(prefix)\(directory)/\(moduleFileName)"
|
||||
return URL(string: "\(base)?v=\(Self.moduleSubscriptionVersion)")!
|
||||
}
|
||||
|
||||
var launchURL: URL? {
|
||||
|
||||
@@ -73,9 +73,13 @@ final class RealtimeLocationManagerTests: XCTestCase {
|
||||
func testOneShotTimeoutTransitionsToContinuousFallback() async {
|
||||
let driver = FakeRealtimeLocationDriver()
|
||||
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() }
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
await fulfillment(of: [fallbackStarted], timeout: 1)
|
||||
XCTAssertEqual(driver.startUpdatingCallCount, 1)
|
||||
|
||||
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
|
||||
@@ -148,6 +152,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
|
||||
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
|
||||
weak var delegate: CLLocationManagerDelegate?
|
||||
var onRequestLocation: (() -> Void)?
|
||||
var onStartUpdating: (() -> Void)?
|
||||
private(set) var requestAuthorizationCallCount = 0
|
||||
private(set) var requestLocationCallCount = 0
|
||||
private(set) var startUpdatingCallCount = 0
|
||||
@@ -164,6 +169,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
|
||||
|
||||
func startUpdatingLocation() {
|
||||
startUpdatingCallCount += 1
|
||||
onStartUpdating?()
|
||||
}
|
||||
|
||||
func stopUpdatingLocation() {
|
||||
|
||||
@@ -125,16 +125,19 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testClientLinksUseUpstreamModulesAndVerificationLabels() {
|
||||
func testClientLinksUseProjectOwnedModulesAndVerificationLabels() {
|
||||
XCTAssertEqual(
|
||||
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"
|
||||
)
|
||||
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
|
||||
XCTAssertTrue(ThirdPartyProxyClient.surge.verificationText?.contains("尚未验证") == true)
|
||||
XCTAssertEqual(
|
||||
ThirdPartyProxyClient.surge.verificationText,
|
||||
String(localized: "配置已提供,尚未验证")
|
||||
)
|
||||
XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL)
|
||||
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(
|
||||
url: ThirdPartyProxyClient.stash.subscriptionURL,
|
||||
@@ -144,10 +147,10 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
||||
url: ThirdPartyProxyClient.shadowrocket.subscriptionURL,
|
||||
resolvingAgainstBaseURL: false
|
||||
)
|
||||
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.stoverride")
|
||||
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.module")
|
||||
XCTAssertTrue(stashComponents?.queryItems?.isEmpty ?? true)
|
||||
XCTAssertTrue(shadowrocketComponents?.queryItems?.isEmpty ?? true)
|
||||
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.stoverride")
|
||||
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.module")
|
||||
XCTAssertEqual(stashComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
|
||||
XCTAssertEqual(shadowrocketComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
|
||||
XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket")
|
||||
XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge")
|
||||
XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x")
|
||||
|
||||
Executable
+58
@@ -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"
|
||||
@@ -10,8 +10,8 @@ CONTENT="$ROOT/App/ContentView.swift"
|
||||
SETUP="$ROOT/App/FirstSetupView.swift"
|
||||
SETTINGS="$ROOT/App/SettingsView.swift"
|
||||
|
||||
grep -q 'return "APP模式"' "$MODE" || fail "APP mode display name is missing"
|
||||
grep -q 'return "第三方代理模式"' "$MODE" || fail "third-party mode display name is missing"
|
||||
grep -q 'String(localized: "APP模式")' "$MODE" || fail "localized APP 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 '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"
|
||||
@@ -37,8 +37,23 @@ grep -Fq '保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>' "$SETUP" \
|
||||
grep -Fq '清除:GET ?action=clear' "$SETUP" \
|
||||
|| fail "client integration guidance must document the clear action"
|
||||
|
||||
grep -q 'Yu9191/wloc/refs/heads/main/modules' "$MANAGER" \
|
||||
|| fail "third-party subscription must point at upstream Yu9191 modules"
|
||||
grep -q 'raw.githubusercontent.com/xweiba/location-spoofer/main' "$MANAGER" \
|
||||
|| 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.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly"
|
||||
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"
|
||||
grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \
|
||||
|| 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"
|
||||
grep -Fq '当前客户端:\(client.name)' "$SETUP" \
|
||||
|| 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:
|
||||
config = json.load(handle)
|
||||
|
||||
assert config["latestVersion"] == "1.0.5"
|
||||
assert config["latestVersion"] == "1.0.6"
|
||||
assert config["minimumSupportedVersion"] == "1.0.0"
|
||||
assert "shadowrocket" not in 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"
|
||||
grep -Fq 'App 生成的诊断报告' "$BUG_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"
|
||||
grep -Fq 'Label("报告 Bug"' "$SETTINGS" \
|
||||
|| fail "Settings must identify the support action as a bug report"
|
||||
|
||||
@@ -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.
|
||||
Vendored
+19
@@ -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"
|
||||
});
|
||||
}
|
||||
@@ -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"})}}})();
|
||||
+6
File diff suppressed because one or more lines are too long
+11
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+16
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+263
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,20 +1,26 @@
|
||||
# Third-party proxy modules
|
||||
|
||||
Third-party proxy mode (Surge / Quantumult X / Loon / Shadowrocket / Stash /
|
||||
Egern) now relies entirely on the upstream
|
||||
[Yu9191/wloc](https://github.com/Yu9191/wloc) modules. The App no longer
|
||||
maintains or ships project-owned module/script copies, so it never drifts
|
||||
from the upstream protocol.
|
||||
The third-party proxy modules and WLOC scripts are maintained in this
|
||||
repository. The original `Yu9191/wloc` repository was deleted, and its Raw
|
||||
URLs now return HTTP 404, so it is retained only as provenance and is not a
|
||||
runtime dependency.
|
||||
|
||||
## Subscription addresses
|
||||
## Hosted files
|
||||
|
||||
The App builds each client's module subscription URL directly from the
|
||||
upstream repository:
|
||||
- Mirrored module subscriptions: `Resources/ThirdPartyProxyModules/`
|
||||
- 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):
|
||||
`https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>`
|
||||
The App appends `?v=1.0.7` to module subscription URLs, and every module uses
|
||||
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:
|
||||
`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 |
|
||||
|---|---|
|
||||
@@ -24,17 +30,36 @@ upstream repository:
|
||||
| `wloc.lpx` | Loon |
|
||||
| `wloc.stoverride` | Stash |
|
||||
|
||||
No `?v=` cache-bust is appended: the URL points at upstream's latest content,
|
||||
and re-importing the subscription in the proxy client re-fetches it.
|
||||
The hosted files become downloadable from these URLs after this change is
|
||||
merged into `main`. Until then, validate them from the checked-out paths.
|
||||
|
||||
## Script protocol
|
||||
|
||||
The upstream `wloc.js` patches Apple WLOC responses and reads coordinates from
|
||||
the `wloc_settings` persistent key or the module `argument` config. The
|
||||
upstream `wloc-settings.js` implements `wloc-settings/save` (query/clear/save)
|
||||
using `lon`/`lat`/`acc`/`randomRadius` parameters.
|
||||
`wloc.js` patches Apple WLOC responses and reads coordinates from the
|
||||
`wloc_settings` persistent key or the module `argument` config.
|
||||
`wloc-settings.js` implements `wloc-settings/save` (query/clear/save) using
|
||||
`lon`/`lat`/`acc`/`randomRadius` parameters.
|
||||
|
||||
The App's third-party save sends `lon`/`lat`/`acc`, matching the upstream
|
||||
script. Motion-state simulation (fields 11/12) is **not** implemented by the
|
||||
upstream scripts and is unavailable in third-party mode; it remains available
|
||||
in APP mode (built-in proxy).
|
||||
The App's third-party save sends `lon`/`lat`/`acc`. Motion-state simulation
|
||||
(fields 11/12) is unavailable in third-party mode; it remains available in
|
||||
App Mode through the 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.
|
||||
|
||||
Reference in New Issue
Block a user