first commit

This commit is contained in:
RyrieNorth
2026-09-09 18:10:20 +08:00
commit 353859dfbc
35 changed files with 4794 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

+1905
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
#!name=iOS Location Spoofer (Self-Hosted)
#!desc=拦截 Apple 定位服务器回应的 GPS 坐标,替换成自定义位置。【免费开源/严禁倒卖】唯一开源仓库: https://github.com/akudamatata/iOS-Location-Spoofer-Web
#!homepage=https://github.com/akudamatata/iOS-Location-Spoofer-Web
#!author=akudamatata
[Script]
iOS Location Spoofer = 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://你的域名/location-spoofer.js,argument=mode=response&configUrl=https://你的域名/loc.json?token=你的Token&latitude=39.90872&longitude=116.39748&horizontalAccuracy=39&verticalAccuracy=1000&altitude=44&debug=false
[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
+922
View File
@@ -0,0 +1,922 @@
/**
* iOS Location Spoofer Web
*
* Copyright (c) 2026 akudamatata (https://github.com/akudamatata/iOS-Location-Spoofer-Web)
* Licensed under CC BY-NC-SA 4.0
*
* ⚠️【特别声明】:本项目完全免费开源,严禁以任何形式进行二次售卖、转售、商业收费代搭建、打包牟利等行为!
* 若您是通过付费渠道获取本项目的,请立即申请退款并举报不良商家!
*
* 【架构说明】:
* 本脚本为 iOS-Location-Spoofer-Web 项目的核心客户端代理改写模块,专为 Shadowrocket(小火箭)环境设计。
* 本实现采用自主重构的流式 Protobuf 编解码引擎与纯 JS 64 位整数运算(零 BigInt 依赖),
* 原生兼容 iOS 12+ 至 iOS 27+ 各版本系统。
*
* 核心流程:
* 1. 拦截 Apple /clls/wloc 定位服务响应;
* 2. 识别封包容器(ARPC 封包 / Marker 封包 / Synthetic 封包 / Bare Protobuf / 滑窗扫描兜底);
* 3. 采用「WLOC 最小改写(Minimal Rewrite)」策略,仅修改经纬度与水平精度,其余字段原值透传;
* 4. 重新组装并以原始或标准容器格式回包给系统。
*/
(function () {
"use strict";
/* ─────────────────────────────────────────────────────────────
1. 配置与默认参数 (Configuration & Constants)
───────────────────────────────────────────────────────────── */
var CONFIG_DEFAULTS = {
enabled: true,
mode: "response",
latitude: 39.90872,
longitude: 116.39748,
horizontalAccuracy: 39,
verticalAccuracy: 1000,
altitude: 44,
failOpen: true,
debug: false,
rawLimit: 0
};
// Apple 定位私有协议特征常数
var APPLE_SYNTHETIC_PREFIX = new Uint8Array([0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]);
var APPLE_MARKER_SIGNATURE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x00, 0x00]);
var CELL_RESPONSE_TAGS = { 22: true, 24: true };
/* ─────────────────────────────────────────────────────────────
2. 基础字节流与诊断工具 (Byte Stream & Diagnostic Utilities)
───────────────────────────────────────────────────────────── */
var ByteUtils = {
fromArray: function (arr) {
return new Uint8Array(arr);
},
concat: function (chunks) {
var total = 0;
var i;
for (i = 0; i < chunks.length; i += 1) {
total += chunks[i].length;
}
var out = new Uint8Array(total);
var offset = 0;
for (i = 0; i < chunks.length; i += 1) {
out.set(chunks[i], offset);
offset += chunks[i].length;
}
return out;
},
hasPrefix: function (source, prefix) {
if (!source || !prefix || source.length < prefix.length) {
return false;
}
for (var i = 0; i < prefix.length; i += 1) {
if (source[i] !== prefix[i]) {
return false;
}
}
return true;
},
findSequence: function (source, sequence) {
if (!source || !sequence || sequence.length === 0 || source.length < sequence.length) {
return -1;
}
var maxIdx = source.length - sequence.length;
for (var i = 0; i <= maxIdx; i += 1) {
var match = true;
for (var j = 0; j < sequence.length; j += 1) {
if (source[i + j] !== sequence[j]) {
match = false;
break;
}
}
if (match) {
return i;
}
}
return -1;
},
toHex: function (bytes, limit) {
if (!bytes) return "<none>";
var max = Math.min(bytes.length, limit || 16);
var hex = [];
for (var i = 0; i < max; i += 1) {
hex.push(("0" + bytes[i].toString(16)).slice(-2));
}
return hex.join("");
},
toBinaryString: function (bytes) {
if (!bytes) return "";
var chunkSize = 0x8000;
var parts = [];
for (var i = 0; i < bytes.length; i += chunkSize) {
var chunk = bytes.subarray(i, i + chunkSize);
parts.push(String.fromCharCode.apply(null, Array.prototype.slice.call(chunk)));
}
return parts.join("");
},
fromBinaryString: function (str) {
if (!str) return new Uint8Array(0);
var out = new Uint8Array(str.length);
for (var i = 0; i < str.length; i += 1) {
out[i] = str.charCodeAt(i) & 0xff;
}
return out;
},
toBase64: function (bytes) {
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var out = "";
for (var i = 0; i < bytes.length; i += 3) {
var b0 = bytes[i];
var b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
var b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
var triplet = (b0 << 16) | (b1 << 8) | b2;
out += alphabet[(triplet >> 18) & 0x3f];
out += alphabet[(triplet >> 12) & 0x3f];
out += i + 1 < bytes.length ? alphabet[(triplet >> 6) & 0x3f] : "=";
out += i + 2 < bytes.length ? alphabet[triplet & 0x3f] : "=";
}
return out;
},
toUint8Array: function (input) {
if (input == null) return null;
if (input instanceof Uint8Array) return input;
if (typeof ArrayBuffer !== "undefined" && input instanceof ArrayBuffer) return new Uint8Array(input);
if (typeof input === "string") return ByteUtils.fromBinaryString(input);
if (typeof input === "object" && typeof input.length === "number") return new Uint8Array(input);
if (typeof input === "object" && input.bytes && typeof input.bytes.length === "number") return new Uint8Array(input.bytes);
if (typeof input === "object" && input.data && typeof input.data.length === "number") return new Uint8Array(input.data);
return null;
}
};
/* ─────────────────────────────────────────────────────────────
3. 纯 JS 64 位 Varint 编解码器(Pure JS 64-bit Varint)
零 BigInt 依赖,原生兼容 iOS 12+ JavaScriptCore
───────────────────────────────────────────────────────────── */
var Varint64 = {
UINT32_MOD: 4294967296,
MAX_SAFE: 9007199254740991,
fromUnsigned: function (val) {
var num = Number(val);
if (!Number.isFinite(num) || num < 0 || num > Varint64.MAX_SAFE) {
throw new Error("Invalid unsigned int64 value: " + val);
}
return {
low: num >>> 0,
high: Math.floor(num / Varint64.UINT32_MOD) >>> 0
};
},
fromSigned: function (val) {
var num = Math.trunc(Number(val));
if (!Number.isFinite(num) || Math.abs(num) > Varint64.MAX_SAFE) {
throw new Error("Invalid signed int64 value: " + val);
}
if (num >= 0) {
return Varint64.fromUnsigned(num);
}
var pos = Varint64.fromUnsigned(-num);
var low = (~pos.low + 1) >>> 0;
var carry = low === 0 ? 1 : 0;
var high = (~pos.high + carry) >>> 0;
return { low: low, high: high };
},
toSignedNumber: function (words) {
var low = words.low >>> 0;
var high = words.high >>> 0;
if ((high & 0x80000000) === 0) {
return (high >>> 0) * Varint64.UINT32_MOD + (low >>> 0);
}
var magLow = (~low + 1) >>> 0;
var carry = magLow === 0 ? 1 : 0;
var magHigh = (~high + carry) >>> 0;
var mag = magHigh * Varint64.UINT32_MOD + magLow;
return -mag;
},
encodeWords: function (words) {
var low = words.low >>> 0;
var high = words.high >>> 0;
var out = [];
while (high !== 0 || low >= 0x80) {
out.push((low & 0x7f) | 0x80);
low = ((low >>> 7) | (high << 25)) >>> 0;
high = high >>> 7;
}
out.push(low & 0x7f);
return ByteUtils.fromArray(out);
},
encodeUnsigned: function (val) {
return Varint64.encodeWords(Varint64.fromUnsigned(val));
},
encodeSigned: function (val) {
return Varint64.encodeWords(Varint64.fromSigned(val));
},
decode: function (bytes, offset) {
var low = 0;
var high = 0;
var shift = 0;
var current = offset || 0;
var count = 0;
while (current < bytes.length && count < 10) {
var b = bytes[current];
var payload = b & 0x7f;
current += 1;
count += 1;
if (shift < 32) {
low = (low | ((payload << shift) >>> 0)) >>> 0;
if (shift > 25) {
high = (high | (payload >>> (32 - shift))) >>> 0;
}
} else {
high = (high | ((payload << (shift - 32)) >>> 0)) >>> 0;
}
if ((b & 0x80) === 0) {
return { low: low, high: high, offset: current };
}
shift += 7;
}
throw new Error("Truncated or malformed varint");
}
};
/* ─────────────────────────────────────────────────────────────
4. Protobuf 协议处理引擎 (Protobuf Streaming Engine)
───────────────────────────────────────────────────────────── */
var ProtobufEngine = {
readFields: function (bytes) {
var fields = [];
var offset = 0;
while (offset < bytes.length) {
var fieldStart = offset;
var tagVarint = Varint64.decode(bytes, offset);
offset = tagVarint.offset;
var tagVal = tagVarint.low;
var fieldNumber = tagVal >>> 3;
var wireType = tagVal & 0x07;
if (fieldNumber <= 0) {
throw new Error("Invalid protobuf field number: " + fieldNumber);
}
if (wireType === 0) {
// Varint
var valVarint = Varint64.decode(bytes, offset);
var rawBytes = bytes.subarray(fieldStart, valVarint.offset);
var valBytes = bytes.subarray(offset, valVarint.offset);
fields.push({
fieldNumber: fieldNumber,
wireType: wireType,
raw: rawBytes,
valueBytes: valBytes,
varint: valVarint,
int64Value: Varint64.toSignedNumber(valVarint)
});
offset = valVarint.offset;
} else if (wireType === 2) {
// Length-delimited (submessage, bytes, string)
var lenVarint = Varint64.decode(bytes, offset);
offset = lenVarint.offset;
var length = lenVarint.low;
if (offset + length > bytes.length) {
throw new Error("Protobuf length-delimited exceeds buffer bounds");
}
var valueBytes = bytes.subarray(offset, offset + length);
offset += length;
fields.push({
fieldNumber: fieldNumber,
wireType: wireType,
raw: bytes.subarray(fieldStart, offset),
valueBytes: valueBytes
});
} else if (wireType === 5) {
// 32-bit fixed
if (offset + 4 > bytes.length) throw new Error("Protobuf fixed32 truncated");
offset += 4;
fields.push({
fieldNumber: fieldNumber,
wireType: wireType,
raw: bytes.subarray(fieldStart, offset)
});
} else if (wireType === 1) {
// 64-bit fixed
if (offset + 8 > bytes.length) throw new Error("Protobuf fixed64 truncated");
offset += 8;
fields.push({
fieldNumber: fieldNumber,
wireType: wireType,
raw: bytes.subarray(fieldStart, offset)
});
} else {
throw new Error("Unsupported wire type: " + wireType);
}
}
return fields;
},
safeReadFields: function (bytes) {
try {
if (!bytes || bytes.length === 0) return null;
var res = ProtobufEngine.readFields(bytes);
return res.length > 0 ? res : null;
} catch (err) {
return null;
}
},
makeVarintField: function (fieldNumber, signedOrUnsignedVal) {
var tagBytes = Varint64.encodeUnsigned((fieldNumber << 3) | 0);
var valBytes = typeof signedOrUnsignedVal === "number" && signedOrUnsignedVal < 0
? Varint64.encodeSigned(signedOrUnsignedVal)
: Varint64.encodeSigned(signedOrUnsignedVal);
return ByteUtils.concat([tagBytes, valBytes]);
},
makeLengthDelimitedField: function (fieldNumber, payloadBytes) {
var tagBytes = Varint64.encodeUnsigned((fieldNumber << 3) | 2);
var lenBytes = Varint64.encodeUnsigned(payloadBytes.length);
return ByteUtils.concat([tagBytes, lenBytes, payloadBytes]);
}
};
/* ─────────────────────────────────────────────────────────────
5. Apple ARPC 封包处理 (Apple ARPC Framing Codec)
───────────────────────────────────────────────────────────── */
var ArpcCodec = {
readUInt16BE: function (bytes, offset) {
if (offset + 2 > bytes.length) throw new Error("UInt16 out of bounds");
return (bytes[offset] << 8) | bytes[offset + 1];
},
readUInt32BE: function (bytes, offset) {
if (offset + 4 > bytes.length) throw new Error("UInt32 out of bounds");
return (
(bytes[offset] * 0x1000000) +
((bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3])
) >>> 0;
},
writeUInt16BE: function (val) {
return ByteUtils.fromArray([(val >> 8) & 0xff, val & 0xff]);
},
writeUInt32BE: function (val) {
return ByteUtils.fromArray([
(val >>> 24) & 0xff,
(val >>> 16) & 0xff,
(val >>> 8) & 0xff,
val & 0xff
]);
},
readPascalString: function (bytes, state) {
var len = ArpcCodec.readUInt16BE(bytes, state.offset);
state.offset += 2;
if (state.offset + len > bytes.length) throw new Error("ARPC string out of bounds");
var chars = [];
for (var i = 0; i < len; i += 1) {
chars.push(String.fromCharCode(bytes[state.offset + i]));
}
state.offset += len;
return chars.join("");
},
writePascalString: function (str) {
var bytes = new Uint8Array(str.length);
for (var i = 0; i < str.length; i += 1) {
bytes[i] = str.charCodeAt(i) & 0x7f;
}
return ByteUtils.concat([ArpcCodec.writeUInt16BE(bytes.length), bytes]);
},
parse: function (bytes) {
var state = { offset: 0 };
var version = ArpcCodec.readUInt16BE(bytes, state.offset);
state.offset += 2;
var locale = ArpcCodec.readPascalString(bytes, state);
var appIdentifier = ArpcCodec.readPascalString(bytes, state);
var osVersion = ArpcCodec.readPascalString(bytes, state);
var functionId = ArpcCodec.readUInt32BE(bytes, state.offset);
state.offset += 4;
var payloadLength = ArpcCodec.readUInt32BE(bytes, state.offset);
state.offset += 4;
if (state.offset + payloadLength > bytes.length) {
throw new Error("ARPC payload length out of bounds");
}
return {
version: version,
locale: locale,
appIdentifier: appIdentifier,
osVersion: osVersion,
functionId: functionId,
payload: bytes.slice(state.offset, state.offset + payloadLength)
};
},
serialize: function (arpc) {
return ByteUtils.concat([
ArpcCodec.writeUInt16BE(arpc.version),
ArpcCodec.writePascalString(arpc.locale),
ArpcCodec.writePascalString(arpc.appIdentifier),
ArpcCodec.writePascalString(arpc.osVersion),
ArpcCodec.writeUInt32BE(arpc.functionId),
ArpcCodec.writeUInt32BE(arpc.payload.length),
arpc.payload
]);
}
};
/* ─────────────────────────────────────────────────────────────
6. WLOC 定位篡改核心 (WLOC Minimal Spoofer & Raw Scanner)
───────────────────────────────────────────────────────────── */
function coordToInt(deg) {
return Math.round(Number(deg) * 100000000);
}
// 最小改写 Location 子消息:仅替换已存在的 纬度(1)/经度(2)/水平精度(3),原值透传其余所有字段
function patchLocationRecord(locationBytes, config) {
if (!locationBytes || locationBytes.length === 0) return locationBytes;
var fields = ProtobufEngine.readFields(locationBytes);
var hasLat = false;
var hasLon = false;
var i;
for (i = 0; i < fields.length; i += 1) {
if (fields[i].fieldNumber === 1 && fields[i].wireType === 0) hasLat = true;
if (fields[i].fieldNumber === 2 && fields[i].wireType === 0) hasLon = true;
}
// 若无经纬度结构,原样放行,避免破坏响应结构
if (!hasLat || !hasLon) {
return locationBytes;
}
var parts = [];
for (i = 0; i < fields.length; i += 1) {
var f = fields[i];
if (f.fieldNumber === 1 && f.wireType === 0) {
parts.push(ProtobufEngine.makeVarintField(1, coordToInt(config.latitude)));
} else if (f.fieldNumber === 2 && f.wireType === 0) {
parts.push(ProtobufEngine.makeVarintField(2, coordToInt(config.longitude)));
} else if (f.fieldNumber === 3 && f.wireType === 0) {
parts.push(ProtobufEngine.makeVarintField(3, config.horizontalAccuracy));
} else {
parts.push(f.raw);
}
}
return ByteUtils.concat(parts);
}
function patchWifiEntity(wifiBytes, config) {
var fields = ProtobufEngine.readFields(wifiBytes);
var parts = [];
for (var i = 0; i < fields.length; i += 1) {
var f = fields[i];
if (f.fieldNumber === 2 && f.wireType === 2) {
parts.push(ProtobufEngine.makeLengthDelimitedField(2, patchLocationRecord(f.valueBytes, config)));
} else {
parts.push(f.raw);
}
}
return ByteUtils.concat(parts);
}
function patchCellEntity(cellBytes, config) {
var fields = ProtobufEngine.readFields(cellBytes);
var parts = [];
for (var i = 0; i < fields.length; i += 1) {
var f = fields[i];
if (f.fieldNumber === 5 && f.wireType === 2) {
parts.push(ProtobufEngine.makeLengthDelimitedField(5, patchLocationRecord(f.valueBytes, config)));
} else {
parts.push(f.raw);
}
}
return ByteUtils.concat(parts);
}
function patchWlocPayload(payloadBytes, config) {
var fields = ProtobufEngine.readFields(payloadBytes);
var parts = [];
var wifiCount = 0;
var cellCount = 0;
for (var i = 0; i < fields.length; i += 1) {
var f = fields[i];
if (f.fieldNumber === 2 && f.wireType === 2) {
parts.push(ProtobufEngine.makeLengthDelimitedField(2, patchWifiEntity(f.valueBytes, config)));
wifiCount += 1;
} else if (CELL_RESPONSE_TAGS[f.fieldNumber] && f.wireType === 2) {
parts.push(ProtobufEngine.makeLengthDelimitedField(f.fieldNumber, patchCellEntity(f.valueBytes, config)));
cellCount += 1;
} else {
// 关键:其余根级字段(包括 3, 4, 33 等)全量原样透传,不丢弃!
parts.push(f.raw);
}
}
return {
payload: ByteUtils.concat(parts),
wifiCount: wifiCount,
cellCount: cellCount
};
}
function buildSyntheticResponse(payload, prefix) {
var pref = prefix || APPLE_SYNTHETIC_PREFIX;
return ByteUtils.concat([pref, ArpcCodec.writeUInt16BE(payload.length), payload]);
}
function looksLikeWlocPayload(bytes) {
if (!bytes || bytes.length === 0) return false;
var tag = bytes[0];
var fieldNumber = tag >> 3;
var wireType = tag & 0x07;
return fieldNumber > 0 && (wireType === 0 || wireType === 2);
}
// 滑窗特征扫描兜底:当遭遇未知前缀头部时,自动在 0~256 字节内滑窗定位 WLOC protobuf
function scanPatchRawBuffer(responseBytes, config) {
if (!responseBytes || responseBytes.length < 8) {
throw new Error("Response buffer too short for raw scan");
}
var offsets = [];
var i;
var frameLimit = Math.min(96, Math.max(0, responseBytes.length - 10));
for (i = 0; i <= frameLimit; i += 2) offsets.push(i);
var rawLimit = Math.min(256, Math.max(0, responseBytes.length - 4));
for (i = 0; i <= rawLimit; i += 1) {
if (offsets.indexOf(i) < 0) offsets.push(i);
}
for (i = 0; i < offsets.length; i += 1) {
var offset = offsets[i];
try {
var slice = responseBytes.subarray(offset);
if (!looksLikeWlocPayload(slice)) continue;
var patched = patchWlocPayload(slice, config);
if (patched.wifiCount > 0 || patched.cellCount > 0) {
return {
response: buildSyntheticResponse(patched.payload),
payload: patched.payload,
wifiCount: patched.wifiCount,
cellCount: patched.cellCount,
kind: "raw",
offset: offset
};
}
} catch (e) {
// Continue scanning
}
}
throw new Error("Raw scan found no valid patchable WLOC payload");
}
function extractEnvelope(responseBytes) {
if (!responseBytes || responseBytes.length < 2) {
throw new Error("Response body is empty or too short");
}
// 1. Prefixed synthetic format
if (responseBytes.length >= 10 && responseBytes[0] === 0x00 && responseBytes[1] === 0x01 && responseBytes[6] === 0x00 && responseBytes[7] === 0x00) {
var payloadLen = ArpcCodec.readUInt16BE(responseBytes, 8);
if (payloadLen > 0 && 10 + payloadLen <= responseBytes.length) {
var payloadCandidate = responseBytes.subarray(10, 10 + payloadLen);
if (ProtobufEngine.safeReadFields(payloadCandidate) !== null) {
return {
kind: "synthetic",
payload: payloadCandidate,
prefix: responseBytes.subarray(0, 8),
suffix: responseBytes.subarray(10 + payloadLen)
};
}
}
}
// 2. Structured ARPC format
try {
var arpc = ArpcCodec.parse(responseBytes);
if (arpc.payload.length > 0 && ProtobufEngine.safeReadFields(arpc.payload) !== null) {
return {
kind: "arpc",
payload: arpc.payload,
arpc: arpc
};
}
} catch (e) {
// Not standard ARPC
}
// 3. Marker signature format
var markerIdx = ByteUtils.findSequence(responseBytes, APPLE_MARKER_SIGNATURE);
if (markerIdx >= 0) {
var lenOffset = markerIdx + APPLE_MARKER_SIGNATURE.length;
if (lenOffset + 2 <= responseBytes.length) {
var markerLen = ArpcCodec.readUInt16BE(responseBytes, lenOffset);
var pOffset = lenOffset + 2;
if (markerLen > 0 && pOffset + markerLen <= responseBytes.length) {
var markerPayload = responseBytes.subarray(pOffset, pOffset + markerLen);
if (ProtobufEngine.safeReadFields(markerPayload) !== null) {
return {
kind: "marker",
payload: markerPayload,
prefix: responseBytes.subarray(0, markerIdx),
markerAndLen: responseBytes.subarray(markerIdx, pOffset),
suffix: responseBytes.subarray(pOffset + markerLen)
};
}
}
}
}
// 4. Bare protobuf
if (looksLikeWlocPayload(responseBytes) && ProtobufEngine.safeReadFields(responseBytes) !== null) {
return {
kind: "bare",
payload: responseBytes
};
}
return null;
}
function spoofAppleResponse(responseBytes, rawConfig) {
var config = normalizeConfig(rawConfig);
var extraction = null;
var strictError = null;
try {
extraction = extractEnvelope(responseBytes);
} catch (err) {
strictError = err;
}
if (extraction) {
var patched = patchWlocPayload(extraction.payload, config);
if (patched.wifiCount > 0 || patched.cellCount > 0) {
var finalResponse;
if (extraction.kind === "arpc") {
finalResponse = ArpcCodec.serialize({
version: extraction.arpc.version,
locale: extraction.arpc.locale,
appIdentifier: extraction.arpc.appIdentifier,
osVersion: extraction.arpc.osVersion,
functionId: extraction.arpc.functionId,
payload: patched.payload
});
} else if (extraction.kind === "marker") {
var newLen = ArpcCodec.writeUInt16BE(patched.payload.length);
finalResponse = ByteUtils.concat([
extraction.prefix,
extraction.markerAndLen.subarray(0, APPLE_MARKER_SIGNATURE.length),
newLen,
patched.payload,
extraction.suffix
]);
} else {
finalResponse = buildSyntheticResponse(patched.payload, extraction.prefix);
}
return {
response: finalResponse,
payload: patched.payload,
wifiCount: patched.wifiCount,
cellCount: patched.cellCount,
kind: extraction.kind,
prefix: extraction.prefix ? ByteUtils.toHex(extraction.prefix, 8) : ""
};
}
strictError = new Error("No patchable location submessages via " + extraction.kind);
}
// 触发滑窗扫描兜底
var rawScan = scanPatchRawBuffer(responseBytes, config);
return {
response: rawScan.response,
payload: rawScan.payload,
wifiCount: rawScan.wifiCount,
cellCount: rawScan.cellCount,
kind: rawScan.kind,
offset: rawScan.offset,
strictError: strictError ? strictError.message : null
};
}
/* ─────────────────────────────────────────────────────────────
7. 参数解析与配置归一化 (Arguments & Config Normalization)
───────────────────────────────────────────────────────────── */
function normalizeConfig(input) {
var cfg = {};
input = input || {};
for (var k in CONFIG_DEFAULTS) {
if (Object.prototype.hasOwnProperty.call(CONFIG_DEFAULTS, k)) {
cfg[k] = CONFIG_DEFAULTS[k];
}
}
for (var key in input) {
if (Object.prototype.hasOwnProperty.call(input, key) && input[key] !== undefined) {
cfg[key] = input[key];
}
}
cfg.latitude = Number(cfg.latitude);
cfg.longitude = Number(cfg.longitude);
cfg.horizontalAccuracy = Number(cfg.horizontalAccuracy) || 39;
cfg.verticalAccuracy = Number(cfg.verticalAccuracy) || 1000;
cfg.altitude = Number(cfg.altitude) || 44;
cfg.debug = cfg.debug === true || cfg.debug === "true";
cfg.failOpen = cfg.failOpen !== false && cfg.failOpen !== "false";
return cfg;
}
function parseArgumentString(argStr) {
var out = {};
if (!argStr || typeof argStr !== "string") return out;
var configUrlIdx = argStr.indexOf("configUrl=");
if (configUrlIdx >= 0) {
var after = argStr.slice(configUrlIdx + 10);
var endIdx = after.search(/[&,\s]/);
if (endIdx < 0) {
out.configUrl = after;
argStr = argStr.slice(0, configUrlIdx);
} else {
out.configUrl = after.slice(0, endIdx);
argStr = argStr.slice(0, configUrlIdx) + after.slice(endIdx);
}
}
var tokens = argStr.split(/[&,]/);
for (var i = 0; i < tokens.length; i += 1) {
var token = tokens[i].trim();
if (!token) continue;
var eq = token.indexOf("=");
if (eq > 0) {
var key = token.slice(0, eq).trim();
var val = token.slice(eq + 1).trim();
out[key] = val;
}
}
return out;
}
/* ─────────────────────────────────────────────────────────────
8. Shadowrocket 运行时接入 (Shadowrocket Runtime Integration)
───────────────────────────────────────────────────────────── */
function fetchRemoteConfig(url, callback) {
if (!url || typeof $httpClient === "undefined") {
callback(null);
return;
}
$httpClient.get({ url: url, timeout: 5 }, function (err, resp, data) {
if (err || !data || (resp && resp.status >= 400)) {
callback(null);
return;
}
try {
var json = JSON.parse(data);
callback(json);
} catch (e) {
callback(null);
}
});
}
function runShadowrocket() {
var hasResponse = typeof $response !== "undefined" && $response != null;
var hasRequest = typeof $request !== "undefined" && $request != null;
if (!hasResponse && !hasRequest) {
return;
}
var scriptArgs = typeof $argument === "string" ? parseArgumentString($argument) : {};
var config = normalizeConfig(scriptArgs);
function passThrough() {
$done({});
}
function completeResponse(bodyBytes, wifiCount, cellCount) {
var headers = ($response && $response.headers) ? $response.headers : {};
delete headers["Content-Encoding"];
delete headers["content-encoding"];
headers["Content-Length"] = String(bodyBytes.length);
headers["X-Location-Spoofer"] = "active";
headers["X-Location-Spoofer-Wifi"] = String(wifiCount);
headers["X-Location-Spoofer-Cell"] = String(cellCount);
$done({
response: {
status: 200,
headers: headers,
body: bodyBytes
}
});
}
function processRewrite(activeConfig) {
try {
var rawBody = ByteUtils.toUint8Array(($response && $response.body != null) ? $response.body : ($response && $response.bodyBytes));
if (!rawBody || rawBody.length < 2) {
passThrough();
return;
}
var result = spoofAppleResponse(rawBody, activeConfig);
if (activeConfig.debug) {
console.log("[Location Spoofer] Patched " + result.wifiCount + " Wi-Fi, " + result.cellCount + " Cell. Format: " + result.kind);
}
completeResponse(result.response, result.wifiCount, result.cellCount);
} catch (err) {
if (activeConfig.debug) {
console.log("[Location Spoofer] Failed: " + err.message);
}
if (activeConfig.failOpen) {
passThrough();
} else {
$done({
response: {
status: "HTTP/1.1 500 Internal Server Error",
headers: { "Content-Type": "text/plain" },
body: "Location spoofing failed: " + err.message
}
});
}
}
}
if (config.configUrl) {
fetchRemoteConfig(config.configUrl, function (remoteData) {
if (remoteData) {
if (remoteData.latitude != null) config.latitude = Number(remoteData.latitude);
if (remoteData.longitude != null) config.longitude = Number(remoteData.longitude);
if (remoteData.horizontalAccuracy != null) config.horizontalAccuracy = Number(remoteData.horizontalAccuracy);
if (remoteData.verticalAccuracy != null) config.verticalAccuracy = Number(remoteData.verticalAccuracy);
if (remoteData.altitude != null) config.altitude = Number(remoteData.altitude);
}
processRewrite(config);
});
} else {
processRewrite(config);
}
}
/* ─────────────────────────────────────────────────────────────
9. 模块导出与入口 (Module Export & Entry Point)
───────────────────────────────────────────────────────────── */
var api = {
CONFIG_DEFAULTS: CONFIG_DEFAULTS,
APPLE_SYNTHETIC_PREFIX: APPLE_SYNTHETIC_PREFIX,
APPLE_MARKER_SIGNATURE: APPLE_MARKER_SIGNATURE,
ByteUtils: ByteUtils,
Varint64: Varint64,
ProtobufEngine: ProtobufEngine,
ArpcCodec: ArpcCodec,
coordToInt: coordToInt,
normalizeConfig: normalizeConfig,
parseArgumentString: parseArgumentString,
patchLocationRecord: patchLocationRecord,
patchWifiEntity: patchWifiEntity,
patchCellEntity: patchCellEntity,
patchWlocPayload: patchWlocPayload,
buildSyntheticResponse: buildSyntheticResponse,
scanPatchRawBuffer: scanPatchRawBuffer,
extractEnvelope: extractEnvelope,
spoofAppleResponse: spoofAppleResponse,
uint64ToSignedNumber: Varint64.toSignedNumber,
// 兼容测试套件别名
concatBytes: ByteUtils.concat,
decodeVarint: Varint64.decode,
encodeVarintUnsigned: Varint64.encodeUnsigned,
encodeVarintSignedInt64: Varint64.encodeSigned,
makeVarintField: ProtobufEngine.makeVarintField,
makeLengthDelimitedField: ProtobufEngine.makeLengthDelimitedField,
parseFields: ProtobufEngine.readFields,
buildAppleWLocResponse: buildSyntheticResponse
};
if (typeof module !== "undefined" && module.exports) {
module.exports = api;
} else {
runShadowrocket();
}
}());
+18
View File
@@ -0,0 +1,18 @@
{
"name": "GPS Spoofer",
"short_name": "GPS Spoofer",
"description": "iOS GPS 模拟定位 Web 管理面板",
"start_url": "/",
"display": "standalone",
"background_color": "#f2efe9",
"theme_color": "#f2efe9",
"orientation": "portrait",
"icons": [
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB