diff --git a/App/BugReportView.swift b/App/BugReportView.swift index 39e5e3c..89e1fee 100644 --- a/App/BugReportView.swift +++ b/App/BugReportView.swift @@ -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) + ``` + """ + } } diff --git a/App/DiagnosticsView.swift b/App/DiagnosticsView.swift index b0ecca4..696bd06 100644 --- a/App/DiagnosticsView.swift +++ b/App/DiagnosticsView.swift @@ -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") + } } diff --git a/App/FirstSetupView.swift b/App/FirstSetupView.swift index cd95960..eb11be7 100644 --- a/App/FirstSetupView.swift +++ b/App/FirstSetupView.swift @@ -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))") } } diff --git a/App/MapHomeView.swift b/App/MapHomeView.swift index 809d421..da896b9 100644 --- a/App/MapHomeView.swift +++ b/App/MapHomeView.swift @@ -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 } diff --git a/App/ProxyManager.swift b/App/ProxyManager.swift index eeb7278..2be8a27 100644 --- a/App/ProxyManager.swift +++ b/App/ProxyManager.swift @@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable { enum ProxyError: LocalizedError { case startFailed - var errorDescription: String? { "Go proxy 启动失败" } + var errorDescription: String? { String(localized: "Go proxy 启动失败") } } diff --git a/App/SettingsView.swift b/App/SettingsView.swift index 20a4aa2..665fc19 100644 --- a/App/SettingsView.swift +++ b/App/SettingsView.swift @@ -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) } } diff --git a/App/SetupCoordinator.swift b/App/SetupCoordinator.swift index 16d7f78..c36addd 100644 --- a/App/SetupCoordinator.swift +++ b/App/SetupCoordinator.swift @@ -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-") - log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过") + log(String(localized: "[步骤 B] 检测证书与 WiFi 代理…")) + log(String(localized: " 方式: 请求 baidu.com/paopao-verify-")) + 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) } } } diff --git a/README.en.md b/README.en.md index 922da07..f28d683 100644 --- a/README.en.md +++ b/README.en.md @@ -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) diff --git a/README.md b/README.md index f189c3b..08468e0 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/Resources/ThirdPartyProxyModules/wloc.conf b/Resources/ThirdPartyProxyModules/wloc.conf new file mode 100644 index 0000000..3094769 --- /dev/null +++ b/Resources/ThirdPartyProxyModules/wloc.conf @@ -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 diff --git a/Resources/ThirdPartyProxyModules/wloc.lpx b/Resources/ThirdPartyProxyModules/wloc.lpx new file mode 100644 index 0000000..ef1e301 --- /dev/null +++ b/Resources/ThirdPartyProxyModules/wloc.lpx @@ -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 diff --git a/Resources/ThirdPartyProxyModules/wloc.module b/Resources/ThirdPartyProxyModules/wloc.module new file mode 100644 index 0000000..5d678ad --- /dev/null +++ b/Resources/ThirdPartyProxyModules/wloc.module @@ -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 diff --git a/Resources/ThirdPartyProxyModules/wloc.sgmodule b/Resources/ThirdPartyProxyModules/wloc.sgmodule new file mode 100644 index 0000000..7a3784d --- /dev/null +++ b/Resources/ThirdPartyProxyModules/wloc.sgmodule @@ -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 diff --git a/Resources/ThirdPartyProxyModules/wloc.stoverride b/Resources/ThirdPartyProxyModules/wloc.stoverride new file mode 100644 index 0000000..e45172b --- /dev/null +++ b/Resources/ThirdPartyProxyModules/wloc.stoverride @@ -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 diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index 529b856..06edd13 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -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-" = " Method: request baidu.com/paopao-verify-"; +" 结果判定: 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"; diff --git a/Shared/CertificateAuthorityStore.swift b/Shared/CertificateAuthorityStore.swift index e1c45fa..8c4af76 100644 --- a/Shared/CertificateAuthorityStore.swift +++ b/Shared/CertificateAuthorityStore.swift @@ -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))") } } } diff --git a/Shared/CoreBridge.swift b/Shared/CoreBridge.swift index ab19518..13d5c0e 100644 --- a/Shared/CoreBridge.swift +++ b/Shared/CoreBridge.swift @@ -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: "无法启动本地证书服务") } } } diff --git a/Shared/RuntimeLog.swift b/Shared/RuntimeLog.swift index ab64fd7..16164bc 100644 --- a/Shared/RuntimeLog.swift +++ b/Shared/RuntimeLog.swift @@ -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) + } } } diff --git a/Shared/ThirdPartyProxyManager.swift b/Shared/ThirdPartyProxyManager.swift index 28101e4..9bea9af 100644 --- a/Shared/ThirdPartyProxyManager.swift +++ b/Shared/ThirdPartyProxyManager.swift @@ -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? { diff --git a/Tests/PaopaoLocationSpooferTests/RealtimeLocationManagerTests.swift b/Tests/PaopaoLocationSpooferTests/RealtimeLocationManagerTests.swift index c65db6c..d91852a 100644 --- a/Tests/PaopaoLocationSpooferTests/RealtimeLocationManagerTests.swift +++ b/Tests/PaopaoLocationSpooferTests/RealtimeLocationManagerTests.swift @@ -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() { diff --git a/Tests/PaopaoLocationSpooferTests/ThirdPartyProxyManagerTests.swift b/Tests/PaopaoLocationSpooferTests/ThirdPartyProxyManagerTests.swift index 39aea45..32d0620 100644 --- a/Tests/PaopaoLocationSpooferTests/ThirdPartyProxyManagerTests.swift +++ b/Tests/PaopaoLocationSpooferTests/ThirdPartyProxyManagerTests.swift @@ -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") diff --git a/Tests/localization_contract_test.sh b/Tests/localization_contract_test.sh new file mode 100755 index 0000000..c451e2f --- /dev/null +++ b/Tests/localization_contract_test.sh @@ -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" diff --git a/Tests/third_party_mode_contract_test.sh b/Tests/third_party_mode_contract_test.sh index a85dbb8..bcfdf22 100755 --- a/Tests/third_party_mode_contract_test.sh +++ b/Tests/third_party_mode_contract_test.sh @@ -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" diff --git a/Tests/update_and_contribution_contract_test.sh b/Tests/update_and_contribution_contract_test.sh index 61ade10..41bdcd3 100644 --- a/Tests/update_and_contribution_contract_test.sh +++ b/Tests/update_and_contribution_contract_test.sh @@ -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" diff --git a/ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md b/ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..5779f54 --- /dev/null +++ b/ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md @@ -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. diff --git a/ThirdParty/WlocScripts/build.mjs b/ThirdParty/WlocScripts/build.mjs new file mode 100644 index 0000000..0c6cdaa --- /dev/null +++ b/ThirdParty/WlocScripts/build.mjs @@ -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" + }); +} diff --git a/ThirdParty/WlocScripts/dist/v1/wloc-settings.js b/ThirdParty/WlocScripts/dist/v1/wloc-settings.js new file mode 100644 index 0000000..6e7377d --- /dev/null +++ b/ThirdParty/WlocScripts/dist/v1/wloc-settings.js @@ -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"})}}})(); diff --git a/ThirdParty/WlocScripts/dist/v1/wloc.js b/ThirdParty/WlocScripts/dist/v1/wloc.js new file mode 100644 index 0000000..06ea42d --- /dev/null +++ b/ThirdParty/WlocScripts/dist/v1/wloc.js @@ -0,0 +1,6 @@ +(()=>{function ue(e){let n=e.length;for(;--n>=0;)e[n]=0}var si=0,bn=1,_i=2,hi=3,ci=258,mt=29,De=256,ye=De+1+mt,he=30,Et=19,xn=2*ye+1,te=15,Qe=16,di=7,yt=256,vn=16,kn=17,mn=18,dt=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),Pe=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),ui=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),En=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),wi=512,Y=new Array((ye+2)*2);ue(Y);var ke=new Array(he*2);ue(ke);var Ae=new Array(wi);ue(Ae);var Se=new Array(ci-hi+1);ue(Se);var At=new Array(mt);ue(At);var Ke=new Array(he);ue(Ke);function et(e,n,t,i,a){this.static_tree=e,this.extra_bits=n,this.extra_base=t,this.elems=i,this.max_length=a,this.has_stree=e&&e.length}var yn,An,Sn;function tt(e,n){this.dyn_tree=e,this.max_code=0,this.stat_desc=n}var Tn=e=>e<256?Ae[e]:Ae[256+(e>>>7)],Te=(e,n)=>{e.pending_buf[e.pending++]=n&255,e.pending_buf[e.pending++]=n>>>8&255},C=(e,n,t)=>{e.bi_valid>Qe-t?(e.bi_buf|=n<>Qe-e.bi_valid,e.bi_valid+=t-Qe):(e.bi_buf|=n<{C(e,t[n*2],t[n*2+1])},zn=(e,n)=>{let t=0;do t|=e&1,e>>>=1,t<<=1;while(--n>0);return t>>>1},pi=e=>{e.bi_valid===16?(Te(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):e.bi_valid>=8&&(e.pending_buf[e.pending++]=e.bi_buf&255,e.bi_buf>>=8,e.bi_valid-=8)},gi=(e,n)=>{let t=n.dyn_tree,i=n.max_code,a=n.stat_desc.static_tree,r=n.stat_desc.has_stree,f=n.stat_desc.extra_bits,o=n.stat_desc.extra_base,h=n.stat_desc.max_length,l,s,E,d,_,u,R=0;for(d=0;d<=te;d++)e.bl_count[d]=0;for(t[e.heap[e.heap_max]*2+1]=0,l=e.heap_max+1;lh&&(d=h,R++),t[s*2+1]=d,!(s>i)&&(e.bl_count[d]++,_=0,s>=o&&(_=f[s-o]),u=t[s*2],e.opt_len+=u*(d+_),r&&(e.static_len+=u*(a[s*2+1]+_)));if(R!==0){do{for(d=h-1;e.bl_count[d]===0;)d--;e.bl_count[d]--,e.bl_count[d+1]+=2,e.bl_count[h]--,R-=2}while(R>0);for(d=h;d!==0;d--)for(s=e.bl_count[d];s!==0;)E=e.heap[--l],!(E>i)&&(t[E*2+1]!==d&&(e.opt_len+=(d-t[E*2+1])*t[E*2],t[E*2+1]=d),s--)}},Rn=(e,n,t)=>{let i=new Array(te+1),a=0,r,f;for(r=1;r<=te;r++)a=a+t[r-1]<<1,i[r]=a;for(f=0;f<=n;f++){let o=e[f*2+1];o!==0&&(e[f*2]=zn(i[o]++,o))}},bi=()=>{let e,n,t,i,a,r=new Array(te+1);for(t=0,i=0;i>=7;i{let n;for(n=0;n{e.bi_valid>8?Te(e,e.bi_buf):e.bi_valid>0&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0},Nt=(e,n,t,i)=>{let a=n*2,r=t*2;return e[a]{let i=e.heap[t],a=t<<1;for(;a<=e.heap_len&&(a{let i,a,r=0,f,o;if(e.sym_next!==0)do i=e.pending_buf[e.sym_buf+r++]&255,i+=(e.pending_buf[e.sym_buf+r++]&255)<<8,a=e.pending_buf[e.sym_buf+r++],i===0?H(e,a,n):(f=Se[a],H(e,f+De+1,n),o=dt[f],o!==0&&(a-=At[f],C(e,a,o)),i--,f=Tn(i),H(e,f,t),o=Pe[f],o!==0&&(i-=Ke[f],C(e,i,o)));while(r{let t=n.dyn_tree,i=n.stat_desc.static_tree,a=n.stat_desc.has_stree,r=n.stat_desc.elems,f,o,h=-1,l;for(e.heap_len=0,e.heap_max=xn,f=0;f>1;f>=1;f--)nt(e,t,f);l=r;do f=e.heap[1],e.heap[1]=e.heap[e.heap_len--],nt(e,t,1),o=e.heap[1],e.heap[--e.heap_max]=f,e.heap[--e.heap_max]=o,t[l*2]=t[f*2]+t[o*2],e.depth[l]=(e.depth[f]>=e.depth[o]?e.depth[f]:e.depth[o])+1,t[f*2+1]=t[o*2+1]=l,e.heap[1]=l++,nt(e,t,1);while(e.heap_len>=2);e.heap[--e.heap_max]=e.heap[1],gi(e,n),Rn(t,h,e.bl_count)},Ct=(e,n,t)=>{let i,a=-1,r,f=n[1],o=0,h=7,l=4;for(f===0&&(h=138,l=3),n[(t+1)*2+1]=65535,i=0;i<=t;i++)r=f,f=n[(i+1)*2+1],!(++o{let i,a=-1,r,f=n[1],o=0,h=7,l=4;for(f===0&&(h=138,l=3),i=0;i<=t;i++)if(r=f,f=n[(i+1)*2+1],!(++o{let n;for(Ct(e,e.dyn_ltree,e.l_desc.max_code),Ct(e,e.dyn_dtree,e.d_desc.max_code),ut(e,e.bl_desc),n=Et-1;n>=3&&e.bl_tree[En[n]*2+1]===0;n--);return e.opt_len+=3*(n+1)+5+5+4,n},vi=(e,n,t,i)=>{let a;for(C(e,n-257,5),C(e,t-1,5),C(e,i-4,4),a=0;a{let n=4093624447,t;for(t=0;t<=31;t++,n>>>=1)if(n&1&&e.dyn_ltree[t*2]!==0)return 0;if(e.dyn_ltree[18]!==0||e.dyn_ltree[20]!==0||e.dyn_ltree[26]!==0)return 1;for(t=32;t{Lt||(bi(),Lt=!0),e.l_desc=new tt(e.dyn_ltree,yn),e.d_desc=new tt(e.dyn_dtree,An),e.bl_desc=new tt(e.bl_tree,Sn),e.bi_buf=0,e.bi_valid=0,On(e)},Dn=(e,n,t,i)=>{C(e,(si<<1)+(i?1:0),3),In(e),Te(e,t),Te(e,~t),t&&e.pending_buf.set(e.window.subarray(n,n+t),e.pending),e.pending+=t},Ei=e=>{C(e,bn<<1,3),H(e,yt,Y),pi(e)},yi=(e,n,t,i)=>{let a,r,f=0;e.level>0?(e.strm.data_type===2&&(e.strm.data_type=ki(e)),ut(e,e.l_desc),ut(e,e.d_desc),f=xi(e),a=e.opt_len+3+7>>>3,r=e.static_len+3+7>>>3,r<=a&&(a=r)):a=r=t+5,t+4<=a&&n!==-1?Dn(e,n,t,i):e.strategy===4||r===a?(C(e,(bn<<1)+(i?1:0),3),Zt(e,Y,ke)):(C(e,(_i<<1)+(i?1:0),3),vi(e,e.l_desc.max_code+1,e.d_desc.max_code+1,f+1),Zt(e,e.dyn_ltree,e.dyn_dtree)),On(e),i&&In(e)},Ai=(e,n,t)=>(e.pending_buf[e.sym_buf+e.sym_next++]=n,e.pending_buf[e.sym_buf+e.sym_next++]=n>>8,e.pending_buf[e.sym_buf+e.sym_next++]=t,n===0?e.dyn_ltree[t*2]++:(e.matches++,n--,e.dyn_ltree[(Se[t]+De+1)*2]++,e.dyn_dtree[Tn(n)*2]++),e.sym_next===e.sym_end),Si=mi,Ti=Dn,zi=yi,Ri=Ai,Oi=Ei,Ii={_tr_init:Si,_tr_stored_block:Ti,_tr_flush_block:zi,_tr_tally:Ri,_tr_align:Oi},Di=(e,n,t,i)=>{let a=e&65535|0,r=e>>>16&65535|0,f=0;for(;t!==0;){f=t>2e3?2e3:t,t-=f;do a=a+n[i++]|0,r=r+a|0;while(--f);a%=65521,r%=65521}return a|r<<16|0},ze=Di,Ni=()=>{let e,n=[];for(var t=0;t<256;t++){e=t;for(var i=0;i<8;i++)e=e&1?3988292384^e>>>1:e>>>1;n[t]=e}return n},Zi=new Uint32Array(Ni()),Ci=(e,n,t,i)=>{let a=Zi,r=i+t;e^=-1;for(let f=i;f>>8^a[(e^n[f])&255];return e^-1},I=Ci,ae={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},we={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8},{_tr_init:$i,_tr_stored_block:wt,_tr_flush_block:Li,_tr_tally:W,_tr_align:Ui}=Ii,{Z_NO_FLUSH:q,Z_PARTIAL_FLUSH:Mi,Z_FULL_FLUSH:Fi,Z_FINISH:U,Z_BLOCK:Ut,Z_OK:D,Z_STREAM_END:Mt,Z_STREAM_ERROR:B,Z_DATA_ERROR:Hi,Z_BUF_ERROR:it,Z_DEFAULT_COMPRESSION:Bi,Z_FILTERED:Pi,Z_HUFFMAN_ONLY:Ue,Z_RLE:Ki,Z_FIXED:Yi,Z_DEFAULT_STRATEGY:Xi,Z_UNKNOWN:Gi,Z_DEFLATED:Ge}=we,ji=9,Vi=15,Wi=8,qi=29,Ji=256,pt=Ji+1+qi,Qi=30,ea=19,ta=2*pt+1,na=15,v=3,V=258,P=V+v+1,ia=32,ce=42,St=57,gt=69,bt=73,xt=91,vt=103,ne=113,xe=666,Z=1,pe=2,re=3,ge=4,aa=3,ie=(e,n)=>(e.msg=ae[n],n),Ft=e=>e*2-(e>4?9:0),j=e=>{let n=e.length;for(;--n>=0;)e[n]=0},ra=e=>{let n,t,i,a=e.w_size;n=e.hash_size,i=n;do t=e.head[--i],e.head[i]=t>=a?t-a:0;while(--n);n=a,i=n;do t=e.prev[--i],e.prev[i]=t>=a?t-a:0;while(--n)},la=(e,n,t)=>(n<{let n=e.state,t=n.pending;t>e.avail_out&&(t=e.avail_out),t!==0&&(e.output.set(n.pending_buf.subarray(n.pending_out,n.pending_out+t),e.next_out),e.next_out+=t,n.pending_out+=t,e.total_out+=t,e.avail_out-=t,n.pending-=t,n.pending===0&&(n.pending_out=0))},L=(e,n)=>{Li(e,e.block_start>=0?e.block_start:-1,e.strstart-e.block_start,n),e.block_start=e.strstart,$(e.strm)},A=(e,n)=>{e.pending_buf[e.pending++]=n},be=(e,n)=>{e.pending_buf[e.pending++]=n>>>8&255,e.pending_buf[e.pending++]=n&255},kt=(e,n,t,i)=>{let a=e.avail_in;return a>i&&(a=i),a===0?0:(e.avail_in-=a,n.set(e.input.subarray(e.next_in,e.next_in+a),t),e.state.wrap===1?e.adler=ze(e.adler,n,a,t):e.state.wrap===2&&(e.adler=I(e.adler,n,a,t)),e.next_in+=a,e.total_in+=a,a)},Nn=(e,n)=>{let t=e.max_chain_length,i=e.strstart,a,r,f=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-P?e.strstart-(e.w_size-P):0,l=e.window,s=e.w_mask,E=e.prev,d=e.strstart+V,_=l[i+f-1],u=l[i+f];e.prev_length>=e.good_match&&(t>>=2),o>e.lookahead&&(o=e.lookahead);do if(a=n,!(l[a+f]!==u||l[a+f-1]!==_||l[a]!==l[i]||l[++a]!==l[i+1])){i+=2,a++;do;while(l[++i]===l[++a]&&l[++i]===l[++a]&&l[++i]===l[++a]&&l[++i]===l[++a]&&l[++i]===l[++a]&&l[++i]===l[++a]&&l[++i]===l[++a]&&l[++i]===l[++a]&&if){if(e.match_start=n,f=r,r>=o)break;_=l[i+f-1],u=l[i+f]}}while((n=E[n&s])>h&&--t!==0);return f<=e.lookahead?f:e.lookahead},de=e=>{let n=e.w_size,t,i,a;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=n+(n-P)&&(e.window.set(e.window.subarray(n,n+n-i),0),e.match_start-=n,e.strstart-=n,e.block_start-=n,e.insert>e.strstart&&(e.insert=e.strstart),ra(e),i+=n),e.strm.avail_in===0)break;if(t=kt(e.strm,e.window,e.strstart+e.lookahead,i),e.lookahead+=t,e.lookahead+e.insert>=v)for(a=e.strstart-e.insert,e.ins_h=e.window[a],e.ins_h=J(e,e.ins_h,e.window[a+1]);e.insert&&(e.ins_h=J(e,e.ins_h,e.window[a+v-1]),e.prev[a&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=a,a++,e.insert--,!(e.lookahead+e.insert{let t=e.pending_buf_size-5>e.w_size?e.w_size:e.pending_buf_size-5,i,a,r,f=0,o=e.strm.avail_in;do{if(i=65535,r=e.bi_valid+42>>3,e.strm.avail_outa+e.strm.avail_in&&(i=a+e.strm.avail_in),i>r&&(i=r),i>8,e.pending_buf[e.pending-2]=~i,e.pending_buf[e.pending-1]=~i>>8,$(e.strm),a&&(a>i&&(a=i),e.strm.output.set(e.window.subarray(e.block_start,e.block_start+a),e.strm.next_out),e.strm.next_out+=a,e.strm.avail_out-=a,e.strm.total_out+=a,e.block_start+=a,i-=a),i&&(kt(e.strm,e.strm.output,e.strm.next_out,i),e.strm.next_out+=i,e.strm.avail_out-=i,e.strm.total_out+=i)}while(f===0);return o-=e.strm.avail_in,o&&(o>=e.w_size?(e.matches=2,e.window.set(e.strm.input.subarray(e.strm.next_in-e.w_size,e.strm.next_in),0),e.strstart=e.w_size,e.insert=e.strstart):(e.window_size-e.strstart<=o&&(e.strstart-=e.w_size,e.window.set(e.window.subarray(e.w_size,e.w_size+e.strstart),0),e.matches<2&&e.matches++,e.insert>e.strstart&&(e.insert=e.strstart)),e.window.set(e.strm.input.subarray(e.strm.next_in-o,e.strm.next_in),e.strstart),e.strstart+=o,e.insert+=o>e.w_size-e.insert?e.w_size-e.insert:o),e.block_start=e.strstart),e.high_waterr&&e.block_start>=e.w_size&&(e.block_start-=e.w_size,e.strstart-=e.w_size,e.window.set(e.window.subarray(e.w_size,e.w_size+e.strstart),0),e.matches<2&&e.matches++,r+=e.w_size,e.insert>e.strstart&&(e.insert=e.strstart)),r>e.strm.avail_in&&(r=e.strm.avail_in),r&&(kt(e.strm,e.window,e.strstart,r),e.strstart+=r,e.insert+=r>e.w_size-e.insert?e.w_size-e.insert:r),e.high_water>3,r=e.pending_buf_size-r>65535?65535:e.pending_buf_size-r,t=r>e.w_size?e.w_size:r,a=e.strstart-e.block_start,(a>=t||(a||n===U)&&n!==q&&e.strm.avail_in===0&&a<=r)&&(i=a>r?r:a,f=n===U&&e.strm.avail_in===0&&i===a?1:0,wt(e,e.block_start,i,f),e.block_start+=i,$(e.strm)),f?re:Z)},at=(e,n)=>{let t,i;for(;;){if(e.lookahead=v&&(e.ins_h=J(e,e.ins_h,e.window[e.strstart+v-1]),t=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),t!==0&&e.strstart-t<=e.w_size-P&&(e.match_length=Nn(e,t)),e.match_length>=v)if(i=W(e,e.strstart-e.match_start,e.match_length-v),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=v){e.match_length--;do e.strstart++,e.ins_h=J(e,e.ins_h,e.window[e.strstart+v-1]),t=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart;while(--e.match_length!==0);e.strstart++}else e.strstart+=e.match_length,e.match_length=0,e.ins_h=e.window[e.strstart],e.ins_h=J(e,e.ins_h,e.window[e.strstart+1]);else i=W(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++;if(i&&(L(e,!1),e.strm.avail_out===0))return Z}return e.insert=e.strstart{let t,i,a;for(;;){if(e.lookahead=v&&(e.ins_h=J(e,e.ins_h,e.window[e.strstart+v-1]),t=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),e.prev_length=e.match_length,e.prev_match=e.match_start,e.match_length=v-1,t!==0&&e.prev_length4096)&&(e.match_length=v-1)),e.prev_length>=v&&e.match_length<=e.prev_length){a=e.strstart+e.lookahead-v,i=W(e,e.strstart-1-e.prev_match,e.prev_length-v),e.lookahead-=e.prev_length-1,e.prev_length-=2;do++e.strstart<=a&&(e.ins_h=J(e,e.ins_h,e.window[e.strstart+v-1]),t=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart);while(--e.prev_length!==0);if(e.match_available=0,e.match_length=v-1,e.strstart++,i&&(L(e,!1),e.strm.avail_out===0))return Z}else if(e.match_available){if(i=W(e,0,e.window[e.strstart-1]),i&&L(e,!1),e.strstart++,e.lookahead--,e.strm.avail_out===0)return Z}else e.match_available=1,e.strstart++,e.lookahead--}return e.match_available&&(i=W(e,0,e.window[e.strstart-1]),e.match_available=0),e.insert=e.strstart{let t,i,a,r,f=e.window;for(;;){if(e.lookahead<=V){if(de(e),e.lookahead<=V&&n===q)return Z;if(e.lookahead===0)break}if(e.match_length=0,e.lookahead>=v&&e.strstart>0&&(a=e.strstart-1,i=f[a],i===f[++a]&&i===f[++a]&&i===f[++a])){r=e.strstart+V;do;while(i===f[++a]&&i===f[++a]&&i===f[++a]&&i===f[++a]&&i===f[++a]&&i===f[++a]&&i===f[++a]&&i===f[++a]&&ae.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=v?(t=W(e,1,e.match_length-v),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(t=W(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),t&&(L(e,!1),e.strm.avail_out===0))return Z}return e.insert=0,n===U?(L(e,!0),e.strm.avail_out===0?re:ge):e.sym_next&&(L(e,!1),e.strm.avail_out===0)?Z:pe},fa=(e,n)=>{let t;for(;;){if(e.lookahead===0&&(de(e),e.lookahead===0)){if(n===q)return Z;break}if(e.match_length=0,t=W(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,t&&(L(e,!1),e.strm.avail_out===0))return Z}return e.insert=0,n===U?(L(e,!0),e.strm.avail_out===0?re:ge):e.sym_next&&(L(e,!1),e.strm.avail_out===0)?Z:pe};function F(e,n,t,i,a){this.good_length=e,this.max_lazy=n,this.nice_length=t,this.max_chain=i,this.func=a}var ve=[new F(0,0,0,0,Zn),new F(4,4,8,4,at),new F(4,5,16,8,at),new F(4,6,32,32,at),new F(4,4,16,16,se),new F(8,16,32,32,se),new F(8,16,128,128,se),new F(8,32,128,256,se),new F(32,128,258,1024,se),new F(32,258,258,4096,se)],sa=e=>{e.window_size=2*e.w_size,j(e.head),e.max_lazy_match=ve[e.level].max_lazy,e.good_match=ve[e.level].good_length,e.nice_match=ve[e.level].nice_length,e.max_chain_length=ve[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=v-1,e.match_available=0,e.ins_h=0};function _a(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=Ge,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(ta*2),this.dyn_dtree=new Uint16Array((2*Qi+1)*2),this.bl_tree=new Uint16Array((2*ea+1)*2),j(this.dyn_ltree),j(this.dyn_dtree),j(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(na+1),this.heap=new Uint16Array(2*pt+1),j(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(2*pt+1),j(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}var Ne=e=>{if(!e)return 1;let n=e.state;return!n||n.strm!==e||n.status!==ce&&n.status!==St&&n.status!==gt&&n.status!==bt&&n.status!==xt&&n.status!==vt&&n.status!==ne&&n.status!==xe?1:0},Cn=e=>{if(Ne(e))return ie(e,B);e.total_in=e.total_out=0,e.data_type=Gi;let n=e.state;return n.pending=0,n.pending_out=0,n.wrap<0&&(n.wrap=-n.wrap),n.status=n.wrap===2?St:n.wrap?ce:ne,e.adler=n.wrap===2?0:1,n.last_flush=-2,$i(n),D},$n=e=>{let n=Cn(e);return n===D&&sa(e.state),n},ha=(e,n)=>Ne(e)||e.state.wrap!==2?B:(e.state.gzhead=n,D),Ln=(e,n,t,i,a,r)=>{if(!e)return B;let f=1;if(n===Bi&&(n=6),i<0?(f=0,i=-i):i>15&&(f=2,i-=16),a<1||a>ji||t!==Ge||i<8||i>15||n<0||n>9||r<0||r>Yi||i===8&&f!==1)return ie(e,B);i===8&&(i=9);let o=new _a;return e.state=o,o.strm=e,o.status=ce,o.wrap=f,o.gzhead=null,o.w_bits=i,o.w_size=1<Ln(e,n,Ge,Vi,Wi,Xi),da=(e,n)=>{if(Ne(e)||n>Ut||n<0)return e?ie(e,B):B;let t=e.state;if(!e.output||e.avail_in!==0&&!e.input||t.status===xe&&n!==U)return ie(e,e.avail_out===0?it:B);let i=t.last_flush;if(t.last_flush=n,t.pending!==0){if($(e),e.avail_out===0)return t.last_flush=-1,D}else if(e.avail_in===0&&Ft(n)<=Ft(i)&&n!==U)return ie(e,it);if(t.status===xe&&e.avail_in!==0)return ie(e,it);if(t.status===ce&&t.wrap===0&&(t.status=ne),t.status===ce){let a=Ge+(t.w_bits-8<<4)<<8,r=-1;if(t.strategy>=Ue||t.level<2?r=0:t.level<6?r=1:t.level===6?r=2:r=3,a|=r<<6,t.strstart!==0&&(a|=ia),a+=31-a%31,be(t,a),t.strstart!==0&&(be(t,e.adler>>>16),be(t,e.adler&65535)),e.adler=1,t.status=ne,$(e),t.pending!==0)return t.last_flush=-1,D}if(t.status===St){if(e.adler=0,A(t,31),A(t,139),A(t,8),t.gzhead)A(t,(t.gzhead.text?1:0)+(t.gzhead.hcrc?2:0)+(t.gzhead.extra?4:0)+(t.gzhead.name?8:0)+(t.gzhead.comment?16:0)),A(t,t.gzhead.time&255),A(t,t.gzhead.time>>8&255),A(t,t.gzhead.time>>16&255),A(t,t.gzhead.time>>24&255),A(t,t.level===9?2:t.strategy>=Ue||t.level<2?4:0),A(t,t.gzhead.os&255),t.gzhead.extra&&t.gzhead.extra.length&&(A(t,t.gzhead.extra.length&255),A(t,t.gzhead.extra.length>>8&255)),t.gzhead.hcrc&&(e.adler=I(e.adler,t.pending_buf,t.pending,0)),t.gzindex=0,t.status=gt;else if(A(t,0),A(t,0),A(t,0),A(t,0),A(t,0),A(t,t.level===9?2:t.strategy>=Ue||t.level<2?4:0),A(t,aa),t.status=ne,$(e),t.pending!==0)return t.last_flush=-1,D}if(t.status===gt){if(t.gzhead.extra){let a=t.pending,r=(t.gzhead.extra.length&65535)-t.gzindex;for(;t.pending+r>t.pending_buf_size;){let o=t.pending_buf_size-t.pending;if(t.pending_buf.set(t.gzhead.extra.subarray(t.gzindex,t.gzindex+o),t.pending),t.pending=t.pending_buf_size,t.gzhead.hcrc&&t.pending>a&&(e.adler=I(e.adler,t.pending_buf,t.pending-a,a)),t.gzindex+=o,$(e),t.pending!==0)return t.last_flush=-1,D;a=0,r-=o}let f=new Uint8Array(t.gzhead.extra);t.pending_buf.set(f.subarray(t.gzindex,t.gzindex+r),t.pending),t.pending+=r,t.gzhead.hcrc&&t.pending>a&&(e.adler=I(e.adler,t.pending_buf,t.pending-a,a)),t.gzindex=0}t.status=bt}if(t.status===bt){if(t.gzhead.name){let a=t.pending,r;do{if(t.pending===t.pending_buf_size){if(t.gzhead.hcrc&&t.pending>a&&(e.adler=I(e.adler,t.pending_buf,t.pending-a,a)),$(e),t.pending!==0)return t.last_flush=-1,D;a=0}t.gzindexa&&(e.adler=I(e.adler,t.pending_buf,t.pending-a,a)),t.gzindex=0}t.status=xt}if(t.status===xt){if(t.gzhead.comment){let a=t.pending,r;do{if(t.pending===t.pending_buf_size){if(t.gzhead.hcrc&&t.pending>a&&(e.adler=I(e.adler,t.pending_buf,t.pending-a,a)),$(e),t.pending!==0)return t.last_flush=-1,D;a=0}t.gzindexa&&(e.adler=I(e.adler,t.pending_buf,t.pending-a,a))}t.status=vt}if(t.status===vt){if(t.gzhead.hcrc){if(t.pending+2>t.pending_buf_size&&($(e),t.pending!==0))return t.last_flush=-1,D;A(t,e.adler&255),A(t,e.adler>>8&255),e.adler=0}if(t.status=ne,$(e),t.pending!==0)return t.last_flush=-1,D}if(e.avail_in!==0||t.lookahead!==0||n!==q&&t.status!==xe){let a=t.level===0?Zn(t,n):t.strategy===Ue?fa(t,n):t.strategy===Ki?oa(t,n):ve[t.level].func(t,n);if((a===re||a===ge)&&(t.status=xe),a===Z||a===re)return e.avail_out===0&&(t.last_flush=-1),D;if(a===pe&&(n===Mi?Ui(t):n!==Ut&&(wt(t,0,0,!1),n===Fi&&(j(t.head),t.lookahead===0&&(t.strstart=0,t.block_start=0,t.insert=0))),$(e),e.avail_out===0))return t.last_flush=-1,D}return n!==U?D:t.wrap<=0?Mt:(t.wrap===2?(A(t,e.adler&255),A(t,e.adler>>8&255),A(t,e.adler>>16&255),A(t,e.adler>>24&255),A(t,e.total_in&255),A(t,e.total_in>>8&255),A(t,e.total_in>>16&255),A(t,e.total_in>>24&255)):(be(t,e.adler>>>16),be(t,e.adler&65535)),$(e),t.wrap>0&&(t.wrap=-t.wrap),t.pending!==0?D:Mt)},ua=e=>{if(Ne(e))return B;let n=e.state.status;return e.state=null,n===ne?ie(e,Hi):D},wa=(e,n)=>{let t=n.length;if(Ne(e))return B;let i=e.state,a=i.wrap;if(a===2||a===1&&i.status!==ce||i.lookahead)return B;if(a===1&&(e.adler=ze(e.adler,n,t,0)),i.wrap=0,t>=i.w_size){a===0&&(j(i.head),i.strstart=0,i.block_start=0,i.insert=0);let h=new Uint8Array(i.w_size);h.set(n.subarray(t-i.w_size,t),0),n=h,t=i.w_size}let r=e.avail_in,f=e.next_in,o=e.input;for(e.avail_in=t,e.next_in=0,e.input=n,de(i);i.lookahead>=v;){let h=i.strstart,l=i.lookahead-(v-1);do i.ins_h=J(i,i.ins_h,i.window[h+v-1]),i.prev[h&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=h,h++;while(--l);i.strstart=h,i.lookahead=v-1,de(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=v-1,i.match_available=0,e.next_in=f,e.input=o,e.avail_in=r,i.wrap=a,D},pa=ca,ga=Ln,ba=$n,xa=Cn,va=ha,ka=da,ma=ua,Ea=wa,ya="pako deflate (from Nodeca project)",me={deflateInit:pa,deflateInit2:ga,deflateReset:ba,deflateResetKeep:xa,deflateSetHeader:va,deflate:ka,deflateEnd:ma,deflateSetDictionary:Ea,deflateInfo:ya},Aa=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),Sa=function(e){let n=Array.prototype.slice.call(arguments,1);for(;n.length;){let t=n.shift();if(t){if(typeof t!="object")throw new TypeError(t+"must be non-object");for(let i in t)Aa(t,i)&&(e[i]=t[i])}}return e},Ta=e=>{let n=0;for(let i=0,a=e.length;i=252?6:e>=248?5:e>=240?4:e>=224?3:e>=192?2:1;Re[254]=Re[254]=1;var za=e=>{if(typeof TextEncoder=="function"&&TextEncoder.prototype.encode)return new TextEncoder().encode(e);let n,t,i,a,r,f=e.length,o=0;for(a=0;a>>6,n[r++]=128|t&63):t<65536?(n[r++]=224|t>>>12,n[r++]=128|t>>>6&63,n[r++]=128|t&63):(n[r++]=240|t>>>18,n[r++]=128|t>>>12&63,n[r++]=128|t>>>6&63,n[r++]=128|t&63);return n},Ra=(e,n)=>{if(n<65534&&e.subarray&&Un)return String.fromCharCode.apply(null,e.length===n?e:e.subarray(0,n));let t="";for(let i=0;i{let t=n||e.length;if(typeof TextDecoder=="function"&&TextDecoder.prototype.decode)return new TextDecoder().decode(e.subarray(0,n));let i,a,r=new Array(t*2);for(a=0,i=0;i4){r[a++]=65533,i+=o-1;continue}for(f&=o===2?31:o===3?15:7;o>1&&i1){r[a++]=65533;continue}f<65536?r[a++]=f:(f-=65536,r[a++]=55296|f>>10&1023,r[a++]=56320|f&1023)}return Ra(r,a)},Ia=(e,n)=>{n=n||e.length,n>e.length&&(n=e.length);let t=n-1;for(;t>=0&&(e[t]&192)===128;)t--;return t<0||t===0?n:t+Re[e[t]]>n?t:n},Oe={string2buf:za,buf2string:Oa,utf8border:Ia};function Da(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}var Mn=Da,Fn=Object.prototype.toString,{Z_NO_FLUSH:Na,Z_SYNC_FLUSH:Za,Z_FULL_FLUSH:Ca,Z_FINISH:$a,Z_OK:Ye,Z_STREAM_END:La,Z_DEFAULT_COMPRESSION:Ua,Z_DEFAULT_STRATEGY:Ma,Z_DEFLATED:Fa}=we;function Ze(e){this.options=je.assign({level:Ua,method:Fa,chunkSize:16384,windowBits:15,memLevel:8,strategy:Ma},e||{});let n=this.options;n.raw&&n.windowBits>0?n.windowBits=-n.windowBits:n.gzip&&n.windowBits>0&&n.windowBits<16&&(n.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Mn,this.strm.avail_out=0;let t=me.deflateInit2(this.strm,n.level,n.method,n.windowBits,n.memLevel,n.strategy);if(t!==Ye)throw new Error(ae[t]);if(n.header&&me.deflateSetHeader(this.strm,n.header),n.dictionary){let i;if(typeof n.dictionary=="string"?i=Oe.string2buf(n.dictionary):Fn.call(n.dictionary)==="[object ArrayBuffer]"?i=new Uint8Array(n.dictionary):i=n.dictionary,t=me.deflateSetDictionary(this.strm,i),t!==Ye)throw new Error(ae[t]);this._dict_set=!0}}Ze.prototype.push=function(e,n){let t=this.strm,i=this.options.chunkSize,a,r;if(this.ended)return!1;for(n===~~n?r=n:r=n===!0?$a:Na,typeof e=="string"?t.input=Oe.string2buf(e):Fn.call(e)==="[object ArrayBuffer]"?t.input=new Uint8Array(e):t.input=e,t.next_in=0,t.avail_in=t.input.length;;){if(t.avail_out===0&&(t.output=new Uint8Array(i),t.next_out=0,t.avail_out=i),(r===Za||r===Ca)&&t.avail_out<=6){this.onData(t.output.subarray(0,t.next_out)),t.avail_out=0;continue}if(a=me.deflate(t,r),a===La)return t.next_out>0&&this.onData(t.output.subarray(0,t.next_out)),a=me.deflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===Ye;if(t.avail_out===0){this.onData(t.output);continue}if(r>0&&t.next_out>0){this.onData(t.output.subarray(0,t.next_out)),t.avail_out=0;continue}if(t.avail_in===0)break}return!0};Ze.prototype.onData=function(e){this.chunks.push(e)};Ze.prototype.onEnd=function(e){e===Ye&&(this.result=je.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg};function Tt(e,n){let t=new Ze(n);if(t.push(e,!0),t.err)throw t.msg||ae[t.err];return t.result}function Ha(e,n){return n=n||{},n.raw=!0,Tt(e,n)}function Ba(e,n){return n=n||{},n.gzip=!0,Tt(e,n)}var Pa=Ze,Ka=Tt,Ya=Ha,Xa=Ba,Ga=we,ja={Deflate:Pa,deflate:Ka,deflateRaw:Ya,gzip:Xa,constants:Ga},Me=16209,Va=16191,Wa=function(n,t){let i,a,r,f,o,h,l,s,E,d,_,u,R,k,g,S,b,c,y,O,w,T,m,p,x=n.state;i=n.next_in,m=n.input,a=i+(n.avail_in-5),r=n.next_out,p=n.output,f=r-(t-n.avail_out),o=r+(n.avail_out-257),h=x.dmax,l=x.wsize,s=x.whave,E=x.wnext,d=x.window,_=x.hold,u=x.bits,R=x.lencode,k=x.distcode,g=(1<>>24,_>>>=c,u-=c,c=b>>>16&255,c===0)p[r++]=b&65535;else if(c&16){y=b&65535,c&=15,c&&(u>>=c,u-=c),u<15&&(_+=m[i++]<>>24,_>>>=c,u-=c,c=b>>>16&255,c&16){if(O=b&65535,c&=15,uh){n.msg="invalid distance too far back",x.mode=Me;break e}if(_>>>=c,u-=c,c=r-f,O>c){if(c=O-c,c>s&&x.sane){n.msg="invalid distance too far back",x.mode=Me;break e}if(w=0,T=d,E===0){if(w+=l-c,c2;)p[r++]=T[w++],p[r++]=T[w++],p[r++]=T[w++],y-=3;y&&(p[r++]=T[w++],y>1&&(p[r++]=T[w++]))}else{w=r-O;do p[r++]=p[w++],p[r++]=p[w++],p[r++]=p[w++],y-=3;while(y>2);y&&(p[r++]=p[w++],y>1&&(p[r++]=p[w++]))}}else if((c&64)===0){b=k[(b&65535)+(_&(1<>3,i-=y,u-=y<<3,_&=(1<{let h=o.bits,l=0,s=0,E=0,d=0,_=0,u=0,R=0,k=0,g=0,S=0,b,c,y,O,w,T=null,m,p=new Uint16Array(_e+1),x=new Uint16Array(_e+1),ee=null,Dt,$e,Le;for(l=0;l<=_e;l++)p[l]=0;for(s=0;s=1&&p[d]===0;d--);if(_>d&&(_=d),d===0)return a[r++]=1<<24|64<<16|0,a[r++]=1<<24|64<<16|0,o.bits=1,0;for(E=1;E0&&(e===Pt||d!==1))return-1;for(x[1]=0,l=1;l<_e;l++)x[l+1]=x[l]+p[l];for(s=0;sHt||e===Kt&&g>Bt)return 1;for(;;){Dt=l-R,f[s]+1=m?($e=ee[f[s]-m],Le=T[f[s]-m]):($e=96,Le=0),b=1<>R)+c]=Dt<<24|$e<<16|Le|0;while(c!==0);for(b=1<>=1;if(b!==0?(S&=b-1,S+=b):S=0,s++,--p[l]===0){if(l===d)break;l=n[t+f[s]]}if(l>_&&(S&O)!==y){for(R===0&&(R=_),w+=E,u=l-R,k=1<Ht||e===Kt&&g>Bt)return 1;y=S&O,a[y]=_<<24|u<<16|w-r|0}}return S!==0&&(a[w+S]=l-R<<24|64<<16|0),o.bits=_,0},Ee=tr,nr=0,Hn=1,Bn=2,{Z_FINISH:Yt,Z_BLOCK:ir,Z_TREES:Fe,Z_OK:le,Z_STREAM_END:ar,Z_NEED_DICT:rr,Z_STREAM_ERROR:M,Z_DATA_ERROR:Pn,Z_MEM_ERROR:Kn,Z_BUF_ERROR:lr,Z_DEFLATED:Xt}=we,Ve=16180,Gt=16181,jt=16182,Vt=16183,Wt=16184,qt=16185,Jt=16186,Qt=16187,en=16188,tn=16189,Xe=16190,K=16191,lt=16192,nn=16193,ot=16194,an=16195,rn=16196,ln=16197,on=16198,He=16199,Be=16200,fn=16201,sn=16202,_n=16203,hn=16204,cn=16205,ft=16206,dn=16207,un=16208,z=16209,Yn=16210,Xn=16211,or=852,fr=592,sr=15,_r=sr,wn=e=>(e>>>24&255)+(e>>>8&65280)+((e&65280)<<8)+((e&255)<<24);function hr(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}var oe=e=>{if(!e)return 1;let n=e.state;return!n||n.strm!==e||n.modeXn?1:0},Gn=e=>{if(oe(e))return M;let n=e.state;return e.total_in=e.total_out=n.total=0,e.msg="",n.wrap&&(e.adler=n.wrap&1),n.mode=Ve,n.last=0,n.havedict=0,n.flags=-1,n.dmax=32768,n.head=null,n.hold=0,n.bits=0,n.lencode=n.lendyn=new Int32Array(or),n.distcode=n.distdyn=new Int32Array(fr),n.sane=1,n.back=-1,le},jn=e=>{if(oe(e))return M;let n=e.state;return n.wsize=0,n.whave=0,n.wnext=0,Gn(e)},Vn=(e,n)=>{let t;if(oe(e))return M;let i=e.state;return n<0?(t=0,n=-n):(t=(n>>4)+5,n<48&&(n&=15)),n&&(n<8||n>15)?M:(i.window!==null&&i.wbits!==n&&(i.window=null),i.wrap=t,i.wbits=n,jn(e))},Wn=(e,n)=>{if(!e)return M;let t=new hr;e.state=t,t.strm=e,t.window=null,t.mode=Ve;let i=Vn(e,n);return i!==le&&(e.state=null),i},cr=e=>Wn(e,_r),pn=!0,st,_t,dr=e=>{if(pn){st=new Int32Array(512),_t=new Int32Array(32);let n=0;for(;n<144;)e.lens[n++]=8;for(;n<256;)e.lens[n++]=9;for(;n<280;)e.lens[n++]=7;for(;n<288;)e.lens[n++]=8;for(Ee(Hn,e.lens,0,288,st,0,e.work,{bits:9}),n=0;n<32;)e.lens[n++]=5;Ee(Bn,e.lens,0,32,_t,0,e.work,{bits:5}),pn=!1}e.lencode=st,e.lenbits=9,e.distcode=_t,e.distbits=5},qn=(e,n,t,i)=>{let a,r=e.state;return r.window===null&&(r.wsize=1<=r.wsize?(r.window.set(n.subarray(t-r.wsize,t),0),r.wnext=0,r.whave=r.wsize):(a=r.wsize-r.wnext,a>i&&(a=i),r.window.set(n.subarray(t-i,t-i+a),r.wnext),i-=a,i?(r.window.set(n.subarray(t-i,t),0),r.wnext=i,r.whave=r.wsize):(r.wnext+=a,r.wnext===r.wsize&&(r.wnext=0),r.whave{let t,i,a,r,f,o,h,l,s,E,d,_,u,R,k=0,g,S,b,c,y,O,w,T,m=new Uint8Array(4),p,x,ee=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(oe(e)||!e.output||!e.input&&e.avail_in!==0)return M;t=e.state,t.mode===K&&(t.mode=lt),f=e.next_out,a=e.output,h=e.avail_out,r=e.next_in,i=e.input,o=e.avail_in,l=t.hold,s=t.bits,E=o,d=h,T=le;e:for(;;)switch(t.mode){case Ve:if(t.wrap===0){t.mode=lt;break}for(;s<16;){if(o===0)break e;o--,l+=i[r++]<>>8&255,t.check=I(t.check,m,2,0),l=0,s=0,t.mode=Gt;break}if(t.head&&(t.head.done=!1),!(t.wrap&1)||(((l&255)<<8)+(l>>8))%31){e.msg="incorrect header check",t.mode=z;break}if((l&15)!==Xt){e.msg="unknown compression method",t.mode=z;break}if(l>>>=4,s-=4,w=(l&15)+8,t.wbits===0&&(t.wbits=w),w>15||w>t.wbits){e.msg="invalid window size",t.mode=z;break}t.dmax=1<>8&1),t.flags&512&&t.wrap&4&&(m[0]=l&255,m[1]=l>>>8&255,t.check=I(t.check,m,2,0)),l=0,s=0,t.mode=jt;case jt:for(;s<32;){if(o===0)break e;o--,l+=i[r++]<>>8&255,m[2]=l>>>16&255,m[3]=l>>>24&255,t.check=I(t.check,m,4,0)),l=0,s=0,t.mode=Vt;case Vt:for(;s<16;){if(o===0)break e;o--,l+=i[r++]<>8),t.flags&512&&t.wrap&4&&(m[0]=l&255,m[1]=l>>>8&255,t.check=I(t.check,m,2,0)),l=0,s=0,t.mode=Wt;case Wt:if(t.flags&1024){for(;s<16;){if(o===0)break e;o--,l+=i[r++]<>>8&255,t.check=I(t.check,m,2,0)),l=0,s=0}else t.head&&(t.head.extra=null);t.mode=qt;case qt:if(t.flags&1024&&(_=t.length,_>o&&(_=o),_&&(t.head&&(w=t.head.extra_len-t.length,t.head.extra||(t.head.extra=new Uint8Array(t.head.extra_len)),t.head.extra.set(i.subarray(r,r+_),w)),t.flags&512&&t.wrap&4&&(t.check=I(t.check,i,_,r)),o-=_,r+=_,t.length-=_),t.length))break e;t.length=0,t.mode=Jt;case Jt:if(t.flags&2048){if(o===0)break e;_=0;do w=i[r+_++],t.head&&w&&t.length<65536&&(t.head.name+=String.fromCharCode(w));while(w&&_>9&1,t.head.done=!0),e.adler=t.check=0,t.mode=K;break;case tn:for(;s<32;){if(o===0)break e;o--,l+=i[r++]<>>=s&7,s-=s&7,t.mode=ft;break}for(;s<3;){if(o===0)break e;o--,l+=i[r++]<>>=1,s-=1,l&3){case 0:t.mode=nn;break;case 1:if(dr(t),t.mode=He,n===Fe){l>>>=2,s-=2;break e}break;case 2:t.mode=rn;break;case 3:e.msg="invalid block type",t.mode=z}l>>>=2,s-=2;break;case nn:for(l>>>=s&7,s-=s&7;s<32;){if(o===0)break e;o--,l+=i[r++]<>>16^65535)){e.msg="invalid stored block lengths",t.mode=z;break}if(t.length=l&65535,l=0,s=0,t.mode=ot,n===Fe)break e;case ot:t.mode=an;case an:if(_=t.length,_){if(_>o&&(_=o),_>h&&(_=h),_===0)break e;a.set(i.subarray(r,r+_),f),o-=_,r+=_,h-=_,f+=_,t.length-=_;break}t.mode=K;break;case rn:for(;s<14;){if(o===0)break e;o--,l+=i[r++]<>>=5,s-=5,t.ndist=(l&31)+1,l>>>=5,s-=5,t.ncode=(l&15)+4,l>>>=4,s-=4,t.nlen>286||t.ndist>30){e.msg="too many length or distance symbols",t.mode=z;break}t.have=0,t.mode=ln;case ln:for(;t.have>>=3,s-=3}for(;t.have<19;)t.lens[ee[t.have++]]=0;if(t.lencode=t.lendyn,t.lenbits=7,p={bits:t.lenbits},T=Ee(nr,t.lens,0,19,t.lencode,0,t.work,p),t.lenbits=p.bits,T){e.msg="invalid code lengths set",t.mode=z;break}t.have=0,t.mode=on;case on:for(;t.have>>24,S=k>>>16&255,b=k&65535,!(g<=s);){if(o===0)break e;o--,l+=i[r++]<>>=g,s-=g,t.lens[t.have++]=b;else{if(b===16){for(x=g+2;s>>=g,s-=g,t.have===0){e.msg="invalid bit length repeat",t.mode=z;break}w=t.lens[t.have-1],_=3+(l&3),l>>>=2,s-=2}else if(b===17){for(x=g+3;s>>=g,s-=g,w=0,_=3+(l&7),l>>>=3,s-=3}else{for(x=g+7;s>>=g,s-=g,w=0,_=11+(l&127),l>>>=7,s-=7}if(t.have+_>t.nlen+t.ndist){e.msg="invalid bit length repeat",t.mode=z;break}for(;_--;)t.lens[t.have++]=w}}if(t.mode===z)break;if(t.lens[256]===0){e.msg="invalid code -- missing end-of-block",t.mode=z;break}if(t.lenbits=9,p={bits:t.lenbits},T=Ee(Hn,t.lens,0,t.nlen,t.lencode,0,t.work,p),t.lenbits=p.bits,T){e.msg="invalid literal/lengths set",t.mode=z;break}if(t.distbits=6,t.distcode=t.distdyn,p={bits:t.distbits},T=Ee(Bn,t.lens,t.nlen,t.ndist,t.distcode,0,t.work,p),t.distbits=p.bits,T){e.msg="invalid distances set",t.mode=z;break}if(t.mode=He,n===Fe)break e;case He:t.mode=Be;case Be:if(o>=6&&h>=258){e.next_out=f,e.avail_out=h,e.next_in=r,e.avail_in=o,t.hold=l,t.bits=s,Wa(e,d),f=e.next_out,a=e.output,h=e.avail_out,r=e.next_in,i=e.input,o=e.avail_in,l=t.hold,s=t.bits,t.mode===K&&(t.back=-1);break}for(t.back=0;k=t.lencode[l&(1<>>24,S=k>>>16&255,b=k&65535,!(g<=s);){if(o===0)break e;o--,l+=i[r++]<>c)],g=k>>>24,S=k>>>16&255,b=k&65535,!(c+g<=s);){if(o===0)break e;o--,l+=i[r++]<>>=c,s-=c,t.back+=c}if(l>>>=g,s-=g,t.back+=g,t.length=b,S===0){t.mode=cn;break}if(S&32){t.back=-1,t.mode=K;break}if(S&64){e.msg="invalid literal/length code",t.mode=z;break}t.extra=S&15,t.mode=fn;case fn:if(t.extra){for(x=t.extra;s>>=t.extra,s-=t.extra,t.back+=t.extra}t.was=t.length,t.mode=sn;case sn:for(;k=t.distcode[l&(1<>>24,S=k>>>16&255,b=k&65535,!(g<=s);){if(o===0)break e;o--,l+=i[r++]<>c)],g=k>>>24,S=k>>>16&255,b=k&65535,!(c+g<=s);){if(o===0)break e;o--,l+=i[r++]<>>=c,s-=c,t.back+=c}if(l>>>=g,s-=g,t.back+=g,S&64){e.msg="invalid distance code",t.mode=z;break}t.offset=b,t.extra=S&15,t.mode=_n;case _n:if(t.extra){for(x=t.extra;s>>=t.extra,s-=t.extra,t.back+=t.extra}if(t.offset>t.dmax){e.msg="invalid distance too far back",t.mode=z;break}t.mode=hn;case hn:if(h===0)break e;if(_=d-h,t.offset>_){if(_=t.offset-_,_>t.whave&&t.sane){e.msg="invalid distance too far back",t.mode=z;break}_>t.wnext?(_-=t.wnext,u=t.wsize-_):u=t.wnext-_,_>t.length&&(_=t.length),R=t.window}else R=a,u=f-t.offset,_=t.length;_>h&&(_=h),h-=_,t.length-=_;do a[f++]=R[u++];while(--_);t.length===0&&(t.mode=Be);break;case cn:if(h===0)break e;a[f++]=t.length,h--,t.mode=Be;break;case ft:if(t.wrap){for(;s<32;){if(o===0)break e;o--,l|=i[r++]<{if(oe(e))return M;let n=e.state;return n.window&&(n.window=null),e.state=null,le},pr=(e,n)=>{if(oe(e))return M;let t=e.state;return(t.wrap&2)===0?M:(t.head=n,n.done=!1,le)},gr=(e,n)=>{let t=n.length,i,a,r;return oe(e)||(i=e.state,i.wrap!==0&&i.mode!==Xe)?M:i.mode===Xe&&(a=1,a=ze(a,n,t,0),a!==i.check)?Pn:(r=qn(e,n,t,t),r?(i.mode=Yn,Kn):(i.havedict=1,le))},br=jn,xr=Vn,vr=Gn,kr=cr,mr=Wn,Er=ur,yr=wr,Ar=pr,Sr=gr,Tr="pako inflate (from Nodeca project)",X={inflateReset:br,inflateReset2:xr,inflateResetKeep:vr,inflateInit:kr,inflateInit2:mr,inflate:Er,inflateEnd:yr,inflateGetHeader:Ar,inflateSetDictionary:Sr,inflateInfo:Tr};function zr(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}var Rr=zr,Jn=Object.prototype.toString,{Z_NO_FLUSH:Or,Z_FINISH:Ir,Z_OK:Ie,Z_STREAM_END:ht,Z_NEED_DICT:ct,Z_STREAM_ERROR:Dr,Z_DATA_ERROR:gn,Z_MEM_ERROR:Nr}=we;function Ce(e){this.options=je.assign({chunkSize:1024*64,windowBits:15,to:""},e||{});let n=this.options;n.raw&&n.windowBits>=0&&n.windowBits<16&&(n.windowBits=-n.windowBits,n.windowBits===0&&(n.windowBits=-15)),n.windowBits>=0&&n.windowBits<16&&!(e&&e.windowBits)&&(n.windowBits+=32),n.windowBits>15&&n.windowBits<48&&(n.windowBits&15)===0&&(n.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Mn,this.strm.avail_out=0;let t=X.inflateInit2(this.strm,n.windowBits);if(t!==Ie)throw new Error(ae[t]);if(this.header=new Rr,X.inflateGetHeader(this.strm,this.header),n.dictionary&&(typeof n.dictionary=="string"?n.dictionary=Oe.string2buf(n.dictionary):Jn.call(n.dictionary)==="[object ArrayBuffer]"&&(n.dictionary=new Uint8Array(n.dictionary)),n.raw&&(t=X.inflateSetDictionary(this.strm,n.dictionary),t!==Ie)))throw new Error(ae[t])}Ce.prototype.push=function(e,n){let t=this.strm,i=this.options.chunkSize,a=this.options.dictionary,r,f,o;if(this.ended)return!1;for(n===~~n?f=n:f=n===!0?Ir:Or,Jn.call(e)==="[object ArrayBuffer]"?t.input=new Uint8Array(e):t.input=e,t.next_in=0,t.avail_in=t.input.length;;){for(t.avail_out===0&&(t.output=new Uint8Array(i),t.next_out=0,t.avail_out=i),r=X.inflate(t,f),r===ct&&a&&(r=X.inflateSetDictionary(t,a),r===Ie?r=X.inflate(t,f):r===gn&&(r=ct));t.avail_in>0&&r===ht&&t.state.wrap>0&&e[t.next_in]!==0;)X.inflateReset(t),r=X.inflate(t,f);switch(r){case Dr:case gn:case ct:case Nr:return this.onEnd(r),this.ended=!0,!1}if(o=t.avail_out,t.next_out&&(t.avail_out===0||r===ht))if(this.options.to==="string"){let h=Oe.utf8border(t.output,t.next_out),l=t.next_out-h,s=Oe.buf2string(t.output,h);t.next_out=l,t.avail_out=i-l,l&&t.output.set(t.output.subarray(h,h+l),0),this.onData(s)}else this.onData(t.output.length===t.next_out?t.output:t.output.subarray(0,t.next_out));if(!(r===Ie&&o===0)){if(r===ht)return r=X.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,!0;if(t.avail_in===0)break}}return!0};Ce.prototype.onData=function(e){this.chunks.push(e)};Ce.prototype.onEnd=function(e){e===Ie&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=je.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg};function zt(e,n){let t=new Ce(n);if(t.push(e),t.err)throw t.msg||ae[t.err];return t.result}function Zr(e,n){return n=n||{},n.raw=!0,zt(e,n)}var Cr=Ce,$r=zt,Lr=Zr,Ur=zt,Mr=we,Fr={Inflate:Cr,inflate:$r,inflateRaw:Lr,ungzip:Ur,constants:Mr},{Deflate:Jr,deflate:Qr,deflateRaw:el,gzip:tl}=ja,{Inflate:nl,inflate:il,inflateRaw:al,ungzip:Hr}=Fr;var Qn=Hr;var ei=Uint8Array.from([0,0,0,1,0,0]),Br=4294967296,N=(...e)=>{let n=e.reduce((a,r)=>a+r.length,0),t=new Uint8Array(n),i=0;for(let a of e)t.set(a,i),i+=a.length;return t},fe=(e,n)=>e.length===n.length&&e.every((t,i)=>t===n[i]);function Rt(e,n){let t=0,i=1;for(let a=0;a<10&&n+a>>0,i=Math.floor(n/Br)>>>0,a=[];do{let r=t&127;t=(t>>>7|(i&127)<<25)>>>0,i>>>=7;let f=i!==0||t!==0;a.push(r|(f?128:0))}while(i!==0||t!==0);return Uint8Array.from(a)}var Q=(e,n)=>G(e<<3|n);function We(e,n){return N(Q(e,2),G(n.length),n)}function qe(e){let n=[],t=0;for(;te.length)throw new Error("truncated fixed64");o=e.slice(t,t+8),t+=8}else if(f===2){let h=Rt(e,t);t=h.next;let l=h.value;if(!Number.isSafeInteger(l)||t+l>e.length)throw new Error("truncated length-delimited field");o=e.slice(t,t+l),t+=l}else if(f===5){if(t+4>e.length)throw new Error("truncated fixed32");o=e.slice(t,t+4),t+=4}else throw new Error(`unsupported wire type ${f}`);n.push({number:r,wireType:f,value:o,raw:e.slice(i,t)})}return n}function ti(e,n,t){let i=qe(e);if(!i.some(h=>h.number===1&&h.wireType===0)||!i.some(h=>h.number===2&&h.wireType===0))return e;let a=!1,r=!1,f=i.map(h=>h.wireType!==0?h.raw:h.number===1?N(Q(1,0),G(Math.round(n.latitude*1e8))):h.number===2?N(Q(2,0),G(Math.round(n.longitude*1e8))):h.number===3?N(Q(3,0),G(n.accuracy)):n.motionSimulationEnabled&&h.number===11?(a=!0,N(Q(11,0),G(63))):n.motionSimulationEnabled&&h.number===12?(r=!0,N(Q(12,0),G(467))):h.raw);n.motionSimulationEnabled&&!a&&f.push(N(Q(11,0),G(63))),n.motionSimulationEnabled&&!r&&f.push(N(Q(12,0),G(467)));let o=N(...f);return fe(o,e)||(t.locations+=1),o}function Pr(e,n,t){let i=qe(e);if(!i.some(o=>o.number===1&&o.wireType===2&&/^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$/.test(Array.from(o.value,h=>String.fromCharCode(h)).join(""))))return e;let r=!1,f=i.map(o=>{if(o.number!==2||o.wireType!==2)return o.raw;let h=ti(o.value,n,t);return r||(r=!fe(h,o.value)),We(2,h)});return r&&(t.wifi+=1),N(...f)}function Kr(e,n,t){let i=!1,a=qe(e).map(r=>{if(r.number!==5||r.wireType!==2)return r.raw;let f=ti(r.value,n,t);return i||(i=!fe(f,r.value)),We(5,f)});return i&&(t.cell+=1),N(...a)}function Je(e,n,t={wifi:0,cell:0,locations:0}){let i=qe(e).map(a=>a.number===2&&a.wireType===2?We(2,Pr(a.value,n,t)):(a.number===22||a.number===24)&&a.wireType===2?We(a.number,Kr(a.value,n,t)):a.raw);return{data:N(...i),stats:t}}var Ot=(e,n)=>e[n]<<8|e[n+1],Yr=(e,n)=>e[n]*16777216+(e[n+1]<<16)+(e[n+2]<<8)+e[n+3]>>>0,ni=e=>Uint8Array.from([e>>>8&255,e&255]),Xr=e=>Uint8Array.from([e>>>24&255,e>>>16&255,e>>>8&255,e&255]);function Gr(e,n){e:for(let t=0;t<=e.length-n.length;t+=1){for(let i=0;ie.length)throw new Error("truncated ARPC string");t+=2+Ot(e,t)}let i=t+4,a=i+4;if(a>e.length)throw new Error("truncated ARPC header");let r=Yr(e,i);if(!r||a+r>e.length)throw new Error("invalid ARPC length");let f=Je(e.slice(a,a+r),n);if(fe(f.data,e.slice(a,a+r)))throw new Error("unchanged ARPC");return{data:N(e.slice(0,i),Xr(f.data.length),f.data,e.slice(a+r)),stats:f.stats}}function Vr(e,n){let t=Gr(e,ei);if(t<0)throw new Error("marker not found");let i=t+ei.length,a=i+2,r=Ot(e,i);if(!r||a+r>e.length)throw new Error("invalid marker length");let f=Je(e.slice(a,a+r),n);if(f.data.length>65535||fe(f.data,e.slice(a,a+r)))throw new Error("unchanged marker");return{data:N(e.slice(0,i),ni(f.data.length),f.data,e.slice(a+r)),stats:f.stats}}function Wr(e,n,t){if(n+10>e.length)throw new Error("short frame");let i=Ot(e,n+8);if(!i||n+10+i>e.length)throw new Error("invalid frame");let a=Je(e.slice(n+10,n+10+i),t);if(a.data.length>65535||fe(a.data,e.slice(n+10,n+10+i)))throw new Error("unchanged frame");return{data:N(e.slice(0,n+8),ni(a.data.length),a.data,e.slice(n+10+i)),stats:a.stats}}function ii(e,n){for(let i of[jr,Vr])try{return i(e,n)}catch(a){}let t=[...new Set([0,2,4,6,8,10,12,14,16,...Array.from({length:Math.min(96,Math.max(0,e.length-10))+1},(i,a)=>a)])];for(let i of t)try{return Wr(e,i,n)}catch(a){}for(let i=0;i<=Math.min(256,e.length);i+=1)try{let a=Je(e.slice(i),n);if(!fe(a.data,e.slice(i)))return{data:N(e.slice(0,i),a.data),stats:a.stats}}catch(a){}throw new Error("no patchable wloc payload found")}var ai="locationSpoofer.settings.v1";function ri(){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 li(e){let n=ri()==="quantumultX"?$prefs.valueForKey(e):$persistentStore.read(e);if(!n)return null;try{return JSON.parse(n)}catch(t){return null}}function oi(){let e=typeof $response=="undefined"?null:$response,n=e&&e.bodyBytes!=null?e.bodyBytes:e&&e.body;return n instanceof ArrayBuffer?new Uint8Array(n):ArrayBuffer.isView(n)?new Uint8Array(n.buffer,n.byteOffset,n.byteLength):typeof n=="string"?Uint8Array.from(n,t=>t.charCodeAt(0)&255):new Uint8Array}function qr(e,n){let t=Object.assign({},e||{});for(let i of["Content-Encoding","content-encoding","Transfer-Encoding","transfer-encoding"])delete t[i];return t["Content-Length"]=String(n),t}function fi(e){let n=typeof $response=="undefined"?{}:$response,t=qr(n.headers,e.length),i=ri();i==="quantumultX"?(delete t["Content-Length"],$done({status:"HTTP/1.1 200 OK",headers:t,bodyBytes:e.buffer})):i==="stash"?$done(Object.assign({},n,{status:200,headers:t,body:e})):$done({response:Object.assign({},n,{status:200,headers:t,body:e})})}function It(){$done({})}try{let e=li(ai),n=oi();if(!e||!e.enabled||!n.length)It();else{let i=n.length>=2&&n[0]===31&&n[1]===139?Qn(n):n,a=ii(i,{latitude:Number(e.latitude),longitude:Number(e.longitude),accuracy:Number(e.accuracy!=null?e.accuracy:25),motionSimulationEnabled:e.motionSimulationEnabled===!0});fi(a.data)}}catch(e){console.log(`[Location Spoofer] ${e&&e.message?e.message:e}`),It()}})(); +/*! Bundled license information: + +pako/dist/pako.esm.mjs: + (*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) *) +*/ diff --git a/ThirdParty/WlocScripts/modules/direct/wloc.conf b/ThirdParty/WlocScripts/modules/direct/wloc.conf new file mode 100644 index 0000000..0702cb1 --- /dev/null +++ b/ThirdParty/WlocScripts/modules/direct/wloc.conf @@ -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 diff --git a/ThirdParty/WlocScripts/modules/direct/wloc.lpx b/ThirdParty/WlocScripts/modules/direct/wloc.lpx new file mode 100644 index 0000000..eb2ad2c --- /dev/null +++ b/ThirdParty/WlocScripts/modules/direct/wloc.lpx @@ -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 diff --git a/ThirdParty/WlocScripts/modules/direct/wloc.module b/ThirdParty/WlocScripts/modules/direct/wloc.module new file mode 100644 index 0000000..f414532 --- /dev/null +++ b/ThirdParty/WlocScripts/modules/direct/wloc.module @@ -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 diff --git a/ThirdParty/WlocScripts/modules/direct/wloc.sgmodule b/ThirdParty/WlocScripts/modules/direct/wloc.sgmodule new file mode 100644 index 0000000..3942743 --- /dev/null +++ b/ThirdParty/WlocScripts/modules/direct/wloc.sgmodule @@ -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 diff --git a/ThirdParty/WlocScripts/modules/direct/wloc.stoverride b/ThirdParty/WlocScripts/modules/direct/wloc.stoverride new file mode 100644 index 0000000..397f689 --- /dev/null +++ b/ThirdParty/WlocScripts/modules/direct/wloc.stoverride @@ -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 diff --git a/ThirdParty/WlocScripts/package-lock.json b/ThirdParty/WlocScripts/package-lock.json new file mode 100644 index 0000000..344233c --- /dev/null +++ b/ThirdParty/WlocScripts/package-lock.json @@ -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)" + } + } +} diff --git a/ThirdParty/WlocScripts/package.json b/ThirdParty/WlocScripts/package.json new file mode 100644 index 0000000..11f7080 --- /dev/null +++ b/ThirdParty/WlocScripts/package.json @@ -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" + } +} diff --git a/ThirdParty/WlocScripts/src/core.js b/ThirdParty/WlocScripts/src/core.js new file mode 100644 index 0000000..15f66f0 --- /dev/null +++ b/ThirdParty/WlocScripts/src/core.js @@ -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 }; diff --git a/ThirdParty/WlocScripts/src/response-entry.js b/ThirdParty/WlocScripts/src/response-entry.js new file mode 100644 index 0000000..49f46ab --- /dev/null +++ b/ThirdParty/WlocScripts/src/response-entry.js @@ -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(); +} diff --git a/ThirdParty/WlocScripts/src/runtime.js b/ThirdParty/WlocScripts/src/runtime.js new file mode 100644 index 0000000..0423b9e --- /dev/null +++ b/ThirdParty/WlocScripts/src/runtime.js @@ -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); +} diff --git a/ThirdParty/WlocScripts/src/settings-entry.js b/ThirdParty/WlocScripts/src/settings-entry.js new file mode 100644 index 0000000..626d0d8 --- /dev/null +++ b/ThirdParty/WlocScripts/src/settings-entry.js @@ -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: "保存配置失败" }); + } +} diff --git a/ThirdParty/WlocScripts/test/bundle-compatibility.test.js b/ThirdParty/WlocScripts/test/bundle-compatibility.test.js new file mode 100644 index 0000000..10b5b71 --- /dev/null +++ b/ThirdParty/WlocScripts/test/bundle-compatibility.test.js @@ -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); +}); diff --git a/ThirdParty/WlocScripts/test/core.test.js b/ThirdParty/WlocScripts/test/core.test.js new file mode 100644 index 0000000..ad1733b --- /dev/null +++ b/ThirdParty/WlocScripts/test/core.test.js @@ -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] + ); +}); diff --git a/docs/THIRD_PARTY_MODULES.md b/docs/THIRD_PARTY_MODULES.md index 8a0dea5..8c4bc04 100644 --- a/docs/THIRD_PARTY_MODULES.md +++ b/docs/THIRD_PARTY_MODULES.md @@ -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/` +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/?v=1.0.7` - direct: - `https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/` + `https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/modules/direct/?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.