|
|
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, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
res.set('Content-Type', 'text/html; charset=utf-8'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let html = response.data; |
|
|
|
|
|
html = html.replace(/<meta[^>]*http-equiv=["']?X-Frame-Options["']?[^>]*>/gi, ''); |
|
|
console.log(extractConfigFromHtml(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) { |
|
|
|
|
|
const match = htmlString.match(/window\.CONFIG\s*=\s*(\{[\s\S]*?\});\s*</); |
|
|
if (match) { |
|
|
try { |
|
|
|
|
|
|
|
|
|
|
|
const configStr = match[1]; |
|
|
|
|
|
const config = new Function(`return (${configStr})`)(); |
|
|
return config; |
|
|
} catch (e) { |
|
|
console.error('解析 CONFIG 失败:', e); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function processResponse(response) { |
|
|
|
|
|
if (!response || !response.data || !Array.isArray(response.data.list)) { |
|
|
return response; |
|
|
} |
|
|
|
|
|
|
|
|
const processedList = response.data.list.map(item => { |
|
|
if (item && typeof item === 'object' && item.hasOwnProperty('uid')) { |
|
|
return { |
|
|
...item, |
|
|
uuid: decrypt(item.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 = ''; |
|
|
|
|
|
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'; |
|
|
|
|
|
let a = this.alphabet; |
|
|
for (let i = 0; i < this.seps.length; i++) { |
|
|
const idx = a.indexOf(this.seps.charAt(i)); |
|
|
if (idx === -1) { |
|
|
|
|
|
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, ''); |
|
|
|
|
|
|
|
|
let shuffledSeps = this._consistentShuffle(this.seps, this.salt); |
|
|
this.seps = shuffledSeps; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
_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; |
|
|
|
|
|
const tmp = arr[j]; arr[j] = arr[i]; arr[i] = tmp; |
|
|
i--; |
|
|
v = idx + 1; |
|
|
} |
|
|
return arr.join(''); |
|
|
} |
|
|
|
|
|
|
|
|
encode(...numbers) { |
|
|
if (!numbers || numbers.length === 0) return ''; |
|
|
|
|
|
const MAX_ALLOWED = BigInt('9007199254740992'); |
|
|
for (let n of numbers) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
_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) { |
|
|
if (hash === '') return []; |
|
|
|
|
|
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)); |
|
|
} |
|
|
|
|
|
|
|
|
const encoded = this._encode(result); |
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function decrypt(hashStr) { |
|
|
if (!hashStr) return ''; |
|
|
try { |
|
|
const h = new Hashids('1766', 6); |
|
|
const arr = h.decode(hashStr); |
|
|
|
|
|
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'); |
|
|
}); |