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 (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'); });