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;
// 移除可能的 标签
html = html.replace(/]*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');
});