mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-25 08:01:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27af50375d | ||
|
|
a2f46dd166 | ||
|
|
b0d2f0e8e4 | ||
|
|
7cf38363cf |
@@ -41,6 +41,8 @@ type wireField struct {
|
|||||||
|
|
||||||
var macPattern = regexp.MustCompile(`^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$`)
|
var macPattern = regexp.MustCompile(`^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$`)
|
||||||
|
|
||||||
|
var wlocMarker = []byte{0, 0, 0, 1, 0, 0}
|
||||||
|
|
||||||
func minInt(a, b int) int {
|
func minInt(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
@@ -314,6 +316,99 @@ func patchWlocPayload(payload []byte, c wlocCoords, st *patchStats) ([]byte, boo
|
|||||||
return out, changed, nil
|
return out, changed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseARPCPayloadBounds(body []byte) (lengthOffset, payloadOffset, payloadEnd int, err error) {
|
||||||
|
if len(body) < 2 {
|
||||||
|
return 0, 0, 0, errors.New("ARPC body too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 2 // version
|
||||||
|
for range 3 {
|
||||||
|
if offset+2 > len(body) {
|
||||||
|
return 0, 0, 0, errors.New("truncated ARPC string length")
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint16(body[offset : offset+2]))
|
||||||
|
offset += 2
|
||||||
|
if length > len(body)-offset {
|
||||||
|
return 0, 0, 0, errors.New("truncated ARPC string")
|
||||||
|
}
|
||||||
|
offset += length
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionAndLengthBytes = 8
|
||||||
|
if offset+functionAndLengthBytes > len(body) {
|
||||||
|
return 0, 0, 0, errors.New("truncated ARPC header")
|
||||||
|
}
|
||||||
|
lengthOffset = offset + 4
|
||||||
|
payloadOffset = lengthOffset + 4
|
||||||
|
payloadLength := uint64(binary.BigEndian.Uint32(body[lengthOffset:payloadOffset]))
|
||||||
|
if payloadLength == 0 || payloadLength > uint64(len(body)-payloadOffset) {
|
||||||
|
return 0, 0, 0, errors.New("invalid ARPC payload length")
|
||||||
|
}
|
||||||
|
return lengthOffset, payloadOffset, payloadOffset + int(payloadLength), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchARPCFrame(body []byte, c wlocCoords) ([]byte, patchStats, error) {
|
||||||
|
lengthOffset, payloadOffset, payloadEnd, err := parseARPCPayloadBounds(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, patchStats{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var st patchStats
|
||||||
|
payload := body[payloadOffset:payloadEnd]
|
||||||
|
newPayload, changed, err := patchWlocPayload(payload, c, &st)
|
||||||
|
if err != nil {
|
||||||
|
return nil, patchStats{}, err
|
||||||
|
}
|
||||||
|
if !changed || bytes.Equal(newPayload, payload) {
|
||||||
|
return nil, patchStats{}, errors.New("ARPC envelope has no patchable wloc payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var lenBytes [4]byte
|
||||||
|
binary.BigEndian.PutUint32(lenBytes[:], uint32(len(newPayload)))
|
||||||
|
out := append(cloneBytes(body[:lengthOffset]), lenBytes[:]...)
|
||||||
|
out = append(out, newPayload...)
|
||||||
|
out = append(out, body[payloadEnd:]...)
|
||||||
|
return out, st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchMarkerFrame(body []byte, c wlocCoords) ([]byte, patchStats, error) {
|
||||||
|
markerOffset := bytes.Index(body, wlocMarker)
|
||||||
|
if markerOffset < 0 {
|
||||||
|
return nil, patchStats{}, errors.New("wloc marker not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
lengthOffset := markerOffset + len(wlocMarker)
|
||||||
|
payloadOffset := lengthOffset + 2
|
||||||
|
if payloadOffset > len(body) {
|
||||||
|
return nil, patchStats{}, errors.New("truncated marker frame")
|
||||||
|
}
|
||||||
|
payloadLength := int(binary.BigEndian.Uint16(body[lengthOffset:payloadOffset]))
|
||||||
|
if payloadLength == 0 || payloadLength > len(body)-payloadOffset {
|
||||||
|
return nil, patchStats{}, errors.New("invalid marker payload length")
|
||||||
|
}
|
||||||
|
payloadEnd := payloadOffset + payloadLength
|
||||||
|
|
||||||
|
var st patchStats
|
||||||
|
payload := body[payloadOffset:payloadEnd]
|
||||||
|
newPayload, changed, err := patchWlocPayload(payload, c, &st)
|
||||||
|
if err != nil {
|
||||||
|
return nil, patchStats{}, err
|
||||||
|
}
|
||||||
|
if !changed || bytes.Equal(newPayload, payload) {
|
||||||
|
return nil, patchStats{}, errors.New("marker frame has no patchable wloc payload")
|
||||||
|
}
|
||||||
|
if len(newPayload) > 65535 {
|
||||||
|
return nil, patchStats{}, errors.New("patched marker payload too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
var lenBytes [2]byte
|
||||||
|
binary.BigEndian.PutUint16(lenBytes[:], uint16(len(newPayload)))
|
||||||
|
out := append(cloneBytes(body[:lengthOffset]), lenBytes[:]...)
|
||||||
|
out = append(out, newPayload...)
|
||||||
|
out = append(out, body[payloadEnd:]...)
|
||||||
|
return out, st, nil
|
||||||
|
}
|
||||||
|
|
||||||
func patchFrame(body []byte, offset int, c wlocCoords, st *patchStats) ([]byte, patchStats, error) {
|
func patchFrame(body []byte, offset int, c wlocCoords, st *patchStats) ([]byte, patchStats, error) {
|
||||||
if len(body) < offset+10 {
|
if len(body) < offset+10 {
|
||||||
return nil, *st, fmt.Errorf("body too short: %d, base=%d", len(body), offset)
|
return nil, *st, fmt.Errorf("body too short: %d, base=%d", len(body), offset)
|
||||||
@@ -352,6 +447,13 @@ func patchFrame(body []byte, offset int, c wlocCoords, st *patchStats) ([]byte,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func patchWlocBody(body []byte, c wlocCoords) ([]byte, patchStats, error) {
|
func patchWlocBody(body []byte, c wlocCoords) ([]byte, patchStats, error) {
|
||||||
|
if out, st, err := patchARPCFrame(body, c); err == nil {
|
||||||
|
return out, st, nil
|
||||||
|
}
|
||||||
|
if out, st, err := patchMarkerFrame(body, c); err == nil {
|
||||||
|
return out, st, nil
|
||||||
|
}
|
||||||
|
|
||||||
var st patchStats
|
var st patchStats
|
||||||
offsets := []int{0, 2, 4, 6, 8, 10, 12, 14, 16}
|
offsets := []int{0, 2, 4, 6, 8, 10, 12, 14, 16}
|
||||||
seen := map[int]bool{}
|
seen := map[int]bool{}
|
||||||
|
|||||||
+131
-7
@@ -44,6 +44,29 @@ func testFrame(payload []byte) []byte {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testARPCFrame(payload, suffix []byte) ([]byte, int) {
|
||||||
|
var out []byte
|
||||||
|
var version [2]byte
|
||||||
|
binary.BigEndian.PutUint16(version[:], 1)
|
||||||
|
out = append(out, version[:]...)
|
||||||
|
for _, value := range [][]byte{[]byte("zh_CN"), []byte("com.apple.locationd"), []byte("20A123")} {
|
||||||
|
var length [2]byte
|
||||||
|
binary.BigEndian.PutUint16(length[:], uint16(len(value)))
|
||||||
|
out = append(out, length[:]...)
|
||||||
|
out = append(out, value...)
|
||||||
|
}
|
||||||
|
var functionID [4]byte
|
||||||
|
binary.BigEndian.PutUint32(functionID[:], 1)
|
||||||
|
out = append(out, functionID[:]...)
|
||||||
|
lengthOffset := len(out)
|
||||||
|
var payloadLength [4]byte
|
||||||
|
binary.BigEndian.PutUint32(payloadLength[:], uint32(len(payload)))
|
||||||
|
out = append(out, payloadLength[:]...)
|
||||||
|
out = append(out, payload...)
|
||||||
|
out = append(out, suffix...)
|
||||||
|
return out, lengthOffset
|
||||||
|
}
|
||||||
|
|
||||||
func TestPatchWifiLocation(t *testing.T) {
|
func TestPatchWifiLocation(t *testing.T) {
|
||||||
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||||
body := testFrame(payload)
|
body := testFrame(payload)
|
||||||
@@ -69,20 +92,121 @@ func TestPatchWifiLocation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPatchCellLocation(t *testing.T) {
|
func TestPatchCellLocation(t *testing.T) {
|
||||||
cell := writeLengthDelimited(5, testLocation(300, 400, 25))
|
for _, field := range []int{22, 24} {
|
||||||
payload := writeLengthDelimited(22, cell)
|
t.Run(fmt.Sprintf("field_%d", field), func(t *testing.T) {
|
||||||
body := testFrame(payload)
|
cell := writeLengthDelimited(5, testLocation(300, 400, 25))
|
||||||
c := wlocCoords{Latitude: 22.544577, Longitude: 113.94114, Accuracy: 25}
|
payload := writeLengthDelimited(field, cell)
|
||||||
|
body := testFrame(payload)
|
||||||
|
c := wlocCoords{Latitude: 22.544577, Longitude: 113.94114, Accuracy: 25}
|
||||||
|
|
||||||
|
patched, stats, err := patchWlocBody(body, c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stats.Cell != 1 || stats.Locations != 1 {
|
||||||
|
t.Fatalf("unexpected stats: %+v", stats)
|
||||||
|
}
|
||||||
|
if bytes.Equal(patched, body) {
|
||||||
|
t.Fatal("body was not patched")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchARPCFramePreservesEnvelopeAndSuffix(t *testing.T) {
|
||||||
|
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||||
|
suffix := []byte{0xde, 0xad, 0xbe, 0xef}
|
||||||
|
body, lengthOffset := testARPCFrame(payload, suffix)
|
||||||
|
originalPrefix := cloneBytes(body[:lengthOffset])
|
||||||
|
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
|
||||||
|
|
||||||
patched, stats, err := patchWlocBody(body, c)
|
patched, stats, err := patchWlocBody(body, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if stats.Cell != 1 || stats.Locations != 1 {
|
if stats.WiFi != 1 || stats.Locations != 1 {
|
||||||
t.Fatalf("unexpected stats: %+v", stats)
|
t.Fatalf("unexpected stats: %+v", stats)
|
||||||
}
|
}
|
||||||
if bytes.Equal(patched, body) {
|
if !bytes.Equal(patched[:lengthOffset], originalPrefix) {
|
||||||
t.Fatal("body was not patched")
|
t.Fatal("ARPC metadata changed")
|
||||||
|
}
|
||||||
|
newLength := int(binary.BigEndian.Uint32(patched[lengthOffset : lengthOffset+4]))
|
||||||
|
if newLength == len(payload) {
|
||||||
|
t.Fatal("ARPC payload length was not updated")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(patched[lengthOffset+4+newLength:], suffix) {
|
||||||
|
t.Fatal("ARPC suffix changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchARPCPayloadLargerThanUint16(t *testing.T) {
|
||||||
|
padding := writeLengthDelimited(99, bytes.Repeat([]byte{0x7f}, 70_000))
|
||||||
|
location := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||||
|
payload := append(padding, location...)
|
||||||
|
body, lengthOffset := testARPCFrame(payload, nil)
|
||||||
|
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
|
||||||
|
|
||||||
|
patched, stats, err := patchWlocBody(body, c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stats.WiFi != 1 || stats.Locations != 1 {
|
||||||
|
t.Fatalf("unexpected stats: %+v", stats)
|
||||||
|
}
|
||||||
|
newLength := int(binary.BigEndian.Uint32(patched[lengthOffset : lengthOffset+4]))
|
||||||
|
if newLength <= 65535 {
|
||||||
|
t.Fatalf("expected 32-bit ARPC payload length, got %d", newLength)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(patched[lengthOffset+4:lengthOffset+4+newLength], padding) {
|
||||||
|
t.Fatal("unknown ARPC payload field changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchMarkerFramePreservesPrefixAndSuffix(t *testing.T) {
|
||||||
|
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||||
|
prefix := []byte{0xaa, 0xbb, 0xcc}
|
||||||
|
suffix := []byte{0xdd, 0xee}
|
||||||
|
var length [2]byte
|
||||||
|
binary.BigEndian.PutUint16(length[:], uint16(len(payload)))
|
||||||
|
body := append(cloneBytes(prefix), wlocMarker...)
|
||||||
|
body = append(body, length[:]...)
|
||||||
|
body = append(body, payload...)
|
||||||
|
body = append(body, suffix...)
|
||||||
|
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
|
||||||
|
|
||||||
|
patched, stats, err := patchWlocBody(body, c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stats.WiFi != 1 || stats.Locations != 1 {
|
||||||
|
t.Fatalf("unexpected stats: %+v", stats)
|
||||||
|
}
|
||||||
|
lengthOffset := len(prefix) + len(wlocMarker)
|
||||||
|
newLength := int(binary.BigEndian.Uint16(patched[lengthOffset : lengthOffset+2]))
|
||||||
|
if newLength == len(payload) {
|
||||||
|
t.Fatal("marker payload length was not updated")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(patched[:len(prefix)], prefix) {
|
||||||
|
t.Fatal("marker prefix changed")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(patched[lengthOffset+2+newLength:], suffix) {
|
||||||
|
t.Fatal("marker suffix changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchBareWlocPayload(t *testing.T) {
|
||||||
|
body := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||||
|
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
|
||||||
|
|
||||||
|
patched, stats, err := patchWlocBody(body, c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stats.WiFi != 1 || stats.Locations != 1 {
|
||||||
|
t.Fatalf("unexpected stats: %+v", stats)
|
||||||
|
}
|
||||||
|
if len(patched) > 0 && bytes.HasPrefix(patched, []byte{0, 1, 0, 0}) {
|
||||||
|
t.Fatal("bare payload was unexpectedly wrapped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -12,7 +12,7 @@ responses in a controlled test environment.
|
|||||||
[](project.yml)
|
[](project.yml)
|
||||||
[](project.yml)
|
[](project.yml)
|
||||||
[](Core/go.mod)
|
[](Core/go.mod)
|
||||||
[](docs/CHANGELOG.md)
|
[](docs/CHANGELOG.md)
|
||||||
|
|
||||||
[Features](#feature-overview) ·
|
[Features](#feature-overview) ·
|
||||||
[How It Works](#how-it-works) ·
|
[How It Works](#how-it-works) ·
|
||||||
@@ -443,10 +443,17 @@ guarantee for every app or release.
|
|||||||
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:
|
||||||
|
|
||||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||||
|
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||||
|
|
||||||
Community link:
|
Thanks to the following LINUX DO users for their contributions:
|
||||||
|
|
||||||
|
- Bug fixes: [Chen Ze](https://linux.do/u/lixiaobaivv)
|
||||||
|
- Ideas and suggestions: [Alex](https://linux.do/u/_alex), [ye4241](https://linux.do/u/ye4241)
|
||||||
|
|
||||||
|
Links:
|
||||||
|
|
||||||
- [LINUX DO](https://linux.do/)
|
- [LINUX DO](https://linux.do/)
|
||||||
|
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
|
||||||
|
|
||||||
Thanks to the open-source contributors working on iOS location-service research, network proxies, and mobile testing
|
Thanks to the open-source contributors working on iOS location-service research, network proxies, and mobile testing
|
||||||
tools.
|
tools.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
[](project.yml)
|
[](project.yml)
|
||||||
[](project.yml)
|
[](project.yml)
|
||||||
[](Core/go.mod)
|
[](Core/go.mod)
|
||||||
[](docs/CHANGELOG.md)
|
[](docs/CHANGELOG.md)
|
||||||
|
|
||||||
[功能概览](#功能概览) ·
|
[功能概览](#功能概览) ·
|
||||||
[工作原理](#工作原理) ·
|
[工作原理](#工作原理) ·
|
||||||
@@ -431,9 +431,16 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
|
|||||||
核心定位响应处理思路、Go 实现和第三方模块参考自:
|
核心定位响应处理思路、Go 实现和第三方模块参考自:
|
||||||
|
|
||||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||||
|
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||||
|
|
||||||
|
感谢以下 LINUX DO 用户对项目的贡献:
|
||||||
|
|
||||||
|
- 功能修复:[陈泽](https://linux.do/u/lixiaobaivv)
|
||||||
|
- 思路及建议:[Alex](https://linux.do/u/_alex)、[ye4241](https://linux.do/u/ye4241)
|
||||||
|
|
||||||
友链:
|
友链:
|
||||||
|
|
||||||
- [LINUX DO](https://linux.do/)
|
- [LINUX DO](https://linux.do/)
|
||||||
|
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
|
||||||
|
|
||||||
感谢开源社区中参与 iOS 定位服务研究、网络代理和移动端测试工具建设的贡献者。
|
感谢开源社区中参与 iOS 定位服务研究、网络代理和移动端测试工具建设的贡献者。
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct AppRemoteConfiguration: Decodable, Equatable {
|
|||||||
let communityPromptClients: [String]
|
let communityPromptClients: [String]
|
||||||
|
|
||||||
static let fallback = AppRemoteConfiguration(
|
static let fallback = AppRemoteConfiguration(
|
||||||
latestVersion: "1.0.3",
|
latestVersion: "1.0.4",
|
||||||
minimumSupportedVersion: "1.0.0",
|
minimumSupportedVersion: "1.0.0",
|
||||||
communityPromptClients: [
|
communityPromptClients: [
|
||||||
ThirdPartyProxyClient.surge.rawValue,
|
ThirdPartyProxyClient.surge.rawValue,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class AppRemoteConfigurationTests: XCTestCase {
|
|||||||
func testFallbackMatchesCurrentProjectPolicy() {
|
func testFallbackMatchesCurrentProjectPolicy() {
|
||||||
let configuration = AppRemoteConfiguration.fallback
|
let configuration = AppRemoteConfiguration.fallback
|
||||||
|
|
||||||
XCTAssertEqual(configuration.latestVersion, "1.0.3")
|
XCTAssertEqual(configuration.latestVersion, "1.0.4")
|
||||||
XCTAssertEqual(configuration.minimumSupportedVersion, "1.0.0")
|
XCTAssertEqual(configuration.minimumSupportedVersion, "1.0.0")
|
||||||
XCTAssertFalse(configuration.requestsCommunityPrompt(for: .shadowrocket))
|
XCTAssertFalse(configuration.requestsCommunityPrompt(for: .shadowrocket))
|
||||||
for client in ThirdPartyProxyClient.allCases where client != .shadowrocket {
|
for client in ThirdPartyProxyClient.allCases where client != .shadowrocket {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ test -s "$ROOT/docs/onboarding-screenshots/shadowrocket/shadowrocket-module-impo
|
|||||||
grep -q 'presentSuccessfulOperationTip(.activation)' "$ROOT/App/MapHomeView.swift" || fail "third-party save must present the activation tip"
|
grep -q 'presentSuccessfulOperationTip(.activation)' "$ROOT/App/MapHomeView.swift" || fail "third-party save must present the activation tip"
|
||||||
grep -q 'presentSuccessfulOperationTip(.deactivation)' "$ROOT/App/MapHomeView.swift" || fail "third-party clear must present the deactivation tip"
|
grep -q 'presentSuccessfulOperationTip(.deactivation)' "$ROOT/App/MapHomeView.swift" || fail "third-party clear must present the deactivation tip"
|
||||||
grep -q 'if spoofState == .active' "$ROOT/App/MapHomeView.swift" || fail "manual help must follow the shared spoof state"
|
grep -q 'if spoofState == .active' "$ROOT/App/MapHomeView.swift" || fail "manual help must follow the shared spoof state"
|
||||||
grep -q 'MARKETING_VERSION: "1.0.3"' "$ROOT/project.yml" || fail "marketing version must be 1.0.3"
|
grep -q 'MARKETING_VERSION: "1.0.4"' "$ROOT/project.yml" || fail "marketing version must be 1.0.4"
|
||||||
grep -q 'CURRENT_PROJECT_VERSION: "4"' "$ROOT/project.yml" || fail "build version must be 4"
|
grep -q 'CURRENT_PROJECT_VERSION: "5"' "$ROOT/project.yml" || fail "build version must be 5"
|
||||||
|
|
||||||
echo "PASS: third-party proxy mode contract"
|
echo "PASS: third-party proxy mode contract"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import sys
|
|||||||
with open(sys.argv[1], encoding="utf-8") as handle:
|
with open(sys.argv[1], encoding="utf-8") as handle:
|
||||||
config = json.load(handle)
|
config = json.load(handle)
|
||||||
|
|
||||||
assert config["latestVersion"] == "1.0.3"
|
assert config["latestVersion"] == "1.0.4"
|
||||||
assert config["minimumSupportedVersion"] == "1.0.0"
|
assert config["minimumSupportedVersion"] == "1.0.0"
|
||||||
assert "shadowrocket" not in config["communityPromptClients"]
|
assert "shadowrocket" not in config["communityPromptClients"]
|
||||||
assert set(config["communityPromptClients"]) == {
|
assert set(config["communityPromptClients"]) == {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
## 已发布
|
## 已发布
|
||||||
|
|
||||||
|
- [v1.0.4](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.4) — 2026-08-09
|
||||||
- [v1.0.3](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.3) — 2026-08-09
|
- [v1.0.3](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.3) — 2026-08-09
|
||||||
- [v1.0.2](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.2) — 2026-08-07
|
- [v1.0.2](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.2) — 2026-08-07
|
||||||
- [v1.0.1](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.1) — 2026-08-06
|
- [v1.0.1](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.1) — 2026-08-06
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# v1.0.4
|
||||||
|
|
||||||
|
发布日期:2026-08-09
|
||||||
|
|
||||||
|
## 主要更新
|
||||||
|
|
||||||
|
- 增加结构化 ARPC 响应解析,修改定位数据后会正确更新 32 位 payload 长度。
|
||||||
|
- 完善 marker、synthetic 和 bare protobuf 响应兼容,保留原始封装前后缀及未知字段。
|
||||||
|
- 补充 CellTower 字段 22/24、ARPC 大 payload 和多响应格式回归测试。
|
||||||
|
|
||||||
|
## 文档与致谢
|
||||||
|
|
||||||
|
- 整理项目参考、社区贡献和友链结构。
|
||||||
|
- 补充相关定位服务研究项目致谢。
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
- WiFi 和 CellTower 坐标替换逻辑保持不变。
|
||||||
|
- 无法识别的新响应封装会回退原有检测路径,不会截断响应。
|
||||||
|
- 本版本未写入语义尚未确认的运动状态字段。
|
||||||
|
|
||||||
|
## 自签安装
|
||||||
|
|
||||||
|
- Release 附件为未签名 IPA,安装前需要自行签名。
|
||||||
|
- 可使用免费 Apple ID 和 Impactor 完成签名安装,无需付费开发者账号。
|
||||||
|
- 签名时请保留 Bundle ID `com.paopaolabs.location-spoofer`、App Group `group.com.paopaolabs.location-spoofer` 及原有 entitlements。
|
||||||
|
- 免费 Apple ID 签名通常只有 7 天有效期,到期后需要重新签名安装。
|
||||||
|
|
||||||
|
<!-- commit-range: v1.0.3..v1.0.4 -->
|
||||||
+2
-2
@@ -8,8 +8,8 @@ options:
|
|||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
SWIFT_VERSION: "5.9"
|
SWIFT_VERSION: "5.9"
|
||||||
MARKETING_VERSION: "1.0.3"
|
MARKETING_VERSION: "1.0.4"
|
||||||
CURRENT_PROJECT_VERSION: "4"
|
CURRENT_PROJECT_VERSION: "5"
|
||||||
CODE_SIGN_STYLE: Manual
|
CODE_SIGN_STYLE: Manual
|
||||||
CODE_SIGNING_ALLOWED: "NO"
|
CODE_SIGNING_ALLOWED: "NO"
|
||||||
CODE_SIGNING_REQUIRED: "NO"
|
CODE_SIGNING_REQUIRED: "NO"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"latestVersion": "1.0.3",
|
"latestVersion": "1.0.4",
|
||||||
"minimumSupportedVersion": "1.0.0",
|
"minimumSupportedVersion": "1.0.0",
|
||||||
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
|
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user