File size: 12,475 Bytes
5e873f0 0144988 5e873f0 0144988 5e873f0 0144988 5e873f0 0144988 5e873f0 0144988 5e873f0 0144988 5e873f0 0144988 5e873f0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const cors = require('cors');
const app = express();
app.use((req, res, next) => {
console.log(`Request from: ${req.ip}, params: ${JSON.stringify(req.query)}`);
next();
});
app.use(cors());
app.use(express.static('public'));
app.get('/search', async (req, res) => {
const { name } = req.query;
const input = `16566580507931f7c79f67099ddad3a4f0e4ed29a6name${name}page1`;
const sign = crypto.createHash('md5').update(input).digest('hex').toUpperCase();
const url = 'https://app.blued.cn/home/web-recharge/getUserInfoByNickname';
const params = {
name,
page: 1,
sign
};
try {
const response = await axios.get(url, { params });
res.send(processResponse(response.data));
} catch (error) {
console.error(error);
res.status(500).send('Error getting data from Blued');
}
});
app.get('/viewPhone', async (req, res) => {
const { id } = req.query;
const url = 'https://app.blued.cn/msg/phone/list';
const headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Cookie': 'uid=' + id
};
try {
const response = await axios({
url,
headers
});
res.send((response.data));
} catch (error) {
console.error(error);
res.status(500).send('Error getting data from Blued');
}
});
// 新增路由:代理用户主页
app.get('/getDetail', async (req, res) => {
const { id } = req.query;
if (!id) {
return res.status(400).send('Missing id parameter');
}
const targetUrl = `https://app.blued.cn/user`;
const params = { id };
// 模拟真实浏览器请求头(关键!)
const headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
};
try {
const response = await axios({
method: 'GET',
url: targetUrl,
params: params,
headers: headers,
responseType: 'text', // 确保返回文本
timeout: 10000,
// 如果 Blued 使用 gzip,axios 会自动解压
});
// 设置正确的内容类型
res.set('Content-Type', 'text/html; charset=utf-8');
// 可选:移除或修改 CSP/X-Frame-Options(但通常在 HTML meta 或响应头中)
// 注意:响应头由 Blued 控制,我们只能改 HTML 内容
// (可选)尝试移除 HTML 中的 X-Frame-Options meta(如果存在)
let html = response.data;
// 移除可能的 <meta http-equiv="X-Frame-Options"> 标签
html = html.replace(/<meta[^>]*http-equiv=["']?X-Frame-Options["']?[^>]*>/gi, '');
console.log(extractConfigFromHtml(html))
// 返回修改后的 HTML
res.json(extractConfigFromHtml(html));
} catch (error) {
console.error('Proxy error:', error.message);
if (error.response) {
console.error('Blued responded with status:', error.response.status);
}
res.status(500).send('Failed to fetch user page');
}
});
function extractConfigFromHtml(htmlString) {
// 匹配 window.CONFIG = { ... };
const match = htmlString.match(/window\.CONFIG\s*=\s*(\{[\s\S]*?\});\s*</);
if (match) {
try {
// 安全地解析 JSON(注意:这里其实是 JS 对象字面量,不是标准 JSON)
// 但由于 Blued 的 CONFIG 是合法 JS 对象,且无函数,可用 eval(谨慎!)或更安全方式
// 推荐:先替换掉末尾分号,再用 Function 构造器(比 eval 稍安全)
const configStr = match[1];
// 使用 Function 构造器解析(避免污染作用域)
const config = new Function(`return (${configStr})`)();
return config;
} catch (e) {
console.error('解析 CONFIG 失败:', e);
return null;
}
}
return null;
}
// 示例用法(假设你已通过 fetch 或 axios 获取到 html 字符串)
// const html = await response.text();
// const config = extractConfigFromHtml(html);
function processResponse(response) {
// 检查结构是否合法
if (!response || !response.data || !Array.isArray(response.data.list)) {
return response; // 结构不符合,直接返回原数据
}
// 深拷贝并处理 list 中的每个对象
const processedList = response.data.list.map(item => {
if (item && typeof item === 'object' && item.hasOwnProperty('uid')) {
return {
...item,
uuid: decrypt(item.uid)
};
}
// 如果没有 uid,保留原对象
return { ...item };
});
// 返回新结构
return {
...response,
data: {
...response.data,
list: processedList
}
};
}
class Hashids {
constructor(salt = '', minLength = 0, alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123567890') {
this.salt = salt + '';
this.minLength = Math.max(0, parseInt(minLength, 10) || 0);
this.alphabet = '';
// deduplicate alphabet chars (preserve order)
for (let ch of alphabet) if (!this.alphabet.includes(ch)) this.alphabet += ch;
if (this.alphabet.length < 16) throw new Error('alphabet must contain at least 16 unique characters');
if (this.alphabet.includes(' ')) throw new Error('alphabet cannot contains spaces');
this.seps = 'cfhistuCFHISTU';
// remove seps from alphabet (mirror Java logic)
let a = this.alphabet;
for (let i = 0; i < this.seps.length; i++) {
const idx = a.indexOf(this.seps.charAt(i));
if (idx === -1) {
// if not found, replace that position in seps with space (same effect as Java)
this.seps = this.seps.substring(0, i) + ' ' + this.seps.substring(i + 1);
} else {
a = a.substring(0, idx) + ' ' + a.substring(idx + 1);
}
}
a = a.replace(/\s+/g, '');
this.seps = this.seps.replace(/\s+/g, '');
// m21293b(strReplaceAll, this.f22838a);
let shuffledSeps = this._consistentShuffle(this.seps, this.salt);
this.seps = shuffledSeps;
// adjust seps/alphabet according to Java logic
if (!this.seps || (a.length / this.seps.length) > 3.5) {
let ceil = Math.ceil(a.length / 3.5);
if (ceil === 1) ceil = ceil + 1;
if (ceil > this.seps.length) {
const diff = ceil - this.seps.length;
this.seps += a.substring(0, diff);
a = a.substring(diff);
} else {
this.seps = this.seps.substring(0, ceil);
}
}
// now shuffle alphabet with salt
a = this._consistentShuffle(a, this.salt);
const guardCount = Math.ceil(a.length / 12);
if (a.length < 3) {
this.guards = this.seps.substring(0, guardCount);
this.seps = this.seps.substring(guardCount);
} else {
this.guards = a.substring(0, guardCount);
a = a.substring(guardCount);
}
this.alphabet = a;
}
// consistent shuffle m21293b
_consistentShuffle(alphabet, salt) {
if (!salt || salt.length === 0) return alphabet;
const arr = alphabet.split('');
const saltChars = salt.split('').map(c => c.charCodeAt(0));
let i = arr.length - 1;
let v = 0;
let p = 0;
while (i > 0) {
const idx = v % saltChars.length;
const c = saltChars[idx];
p += c;
const j = (c + idx + p) % i;
// swap arr[j] and arr[i]
const tmp = arr[j]; arr[j] = arr[i]; arr[i] = tmp;
i--;
v = idx + 1;
}
return arr.join('');
}
// encode numbers -> hash (m21296a -> m21294b)
encode(...numbers) {
if (!numbers || numbers.length === 0) return '';
// check max like Java: > 9007199254740992L
const MAX_ALLOWED = BigInt('9007199254740992');
for (let n of numbers) {
// accept BigInt or Number or numeric string
let bn = (typeof n === 'bigint') ? n : BigInt(n);
if (bn > MAX_ALLOWED) throw new Error('number can not be greater than 9007199254740992');
}
return this._encode(numbers);
}
_encode(nums) {
// compute i sum
let iSum = 0;
for (let idx = 0; idx < nums.length; idx++) {
const n = BigInt(nums[idx]);
iSum = Number(iSum + Number(n % BigInt(idx + 100)));
}
let alphabet = this.alphabet;
const lotteryChar = alphabet.charAt(iSum % alphabet.length);
let ret = lotteryChar;
for (let i = 0; i < nums.length; i++) {
const n = BigInt(nums[i]);
alphabet = this._consistentShuffle(alphabet, (lotteryChar + this.salt + alphabet).substring(0, alphabet.length));
ret += this._hashNumber(n, alphabet);
if (i + 1 < nums.length) {
const guardIndex = Number((n % BigInt(alphabet.charCodeAt(0) + i)) % BigInt(this.seps.length));
ret += this.seps.charAt(guardIndex);
}
}
// pad with guards/guards logic like Java
if (ret.length < this.minLength) {
ret = this.guards.charAt((ret.charCodeAt(0) + iSum) % this.guards.length) + ret;
if (ret.length < this.minLength) {
ret = ret + this.guards.charAt((iSum + ret.charCodeAt(2 % ret.length)) % this.guards.length);
}
}
// while still less than minLength, expand by shuffling alphabet etc
let half = Math.floor(alphabet.length / 2);
while (ret.length < this.minLength) {
alphabet = this._consistentShuffle(alphabet, alphabet);
let str = alphabet.substring(half) + ret + alphabet.substring(0, half);
const excess = str.length - this.minLength;
if (excess > 0) {
const start = Math.floor(excess / 2);
str = str.substring(start, start + this.minLength);
}
ret = str;
}
return ret;
}
// convert number to string using alphabet (m21291a)
_hashNumber(number, alphabet) {
let num = BigInt(number);
const len = alphabet.length;
let out = '';
if (num === 0n) return alphabet.charAt(0);
while (num > 0n) {
const idx = Number(num % BigInt(len));
out = alphabet.charAt(idx) + out;
num = num / BigInt(len);
}
return out;
}
// decode hash -> array of numbers (m21297a -> m21292a)
decode(hash) {
if (hash === '') return [];
// remove guards
const hashSplit = hash.replace(new RegExp('[' + this.guards.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ']', 'g'), ' ').split(' ').filter(Boolean);
const hashPart = (hashSplit.length === 3 || hashSplit.length === 2) ? hashSplit[1] : hashSplit[0];
const lottery = hashPart.charAt(0);
const parts = hashPart.substring(1).replace(new RegExp('[' + this.seps.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ']', 'g'), ' ').split(' ').filter(Boolean);
const result = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
let alphabet = this._consistentShuffle(this.alphabet, (lottery + this.salt + this.alphabet).substring(0, this.alphabet.length));
result.push(this._unhash(part, alphabet));
}
// verify
const encoded = this._encode(result);
// if verification fails, return empty arr
if (encoded !== hash) return [];
return result;
}
_unhash(input, alphabet) {
let num = 0n;
const len = BigInt(alphabet.length);
for (let i = 0; i < input.length; i++) {
const pos = BigInt(alphabet.indexOf(input.charAt(i)));
num = num * len + pos;
}
return num;
}
}
// --- 对外封装的函数(与 Java 中的 HashidEncryptTool 对应) ---
// decrypt: Java m22018a(String str) -> 传入 hash,返回拼接的数字字符串
function decrypt(hashStr) {
if (!hashStr) return '';
try {
const h = new Hashids('1766', 6);
const arr = h.decode(hashStr);
// arr contains BigInt values. Concatenate them as decimal strings without separator (same as Java append)
return arr.map(x => x.toString()).join('');
} catch (e) {
console.error(e);
return '';
}
}
module.exports = {
HashidsJS: Hashids,
decrypt: decrypt
};
app.listen(3000, '0.0.0.0', () => {
console.log('Server listening on port 80');
}); |