923 lines
34 KiB
JavaScript
923 lines
34 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
}());
|