ucenter-client
Version:
A full-featured UCenter client for Node.js.
211 lines (190 loc) • 6.11 kB
JavaScript
//
// UCenter Client Common Functions
//
const request = require('request');
const iconv = require('iconv-lite');
const UC_CLIENT_VERSION = '1.6.0';
const UC_CLIENT_RELEASE = '210501';
const UserAgent = `UC Client Node v${UC_CLIENT_VERSION}`;
module.exports = function(config) {
const {
md5,
randStr,
urlEncode,
urlDecode
} = require('./utils')(config);
return {
request,
ucAuthCode,
ucApiUrl,
ucApiPost,
ucApiRequestData,
ucApiInput,
};
function ucApiUrl(module, action, arg, extra) {
return config.UC_API + '/index.php?' + ucApiRequestData(module, action, arg, extra);
}
/**
* Do API Post
* Post a message to UCenter Server
* @param {string} module
* @param {string} action
* @param {object} args
* @returns {Promise}
*/
function ucApiPost(module, action, args) {
return new Promise(function(resolve, reject) {
const url = config.UC_API + '/index.php';
const postData = ucApiRequestData(module, action, buildPostQS(args));
request.post(url, {
headers: {
'Content-type': 'application/x-www-form-urlencoded',
'User-Agent': UserAgent,
'Connection': 'Close',
'Cache-Control': 'no-cache',
'Cookie': ''
},
body: postData,
encoding: null,
timeout: 20000
}, function(err, req, body) {
if (err) {
return reject('连接用户服务器失败');
} else if (body.indexOf('Access Denied') === 0) {
return reject('无权限访问接口');
} else if (body.indexOf('Access denied for agent changed') === 0) {
return reject('通信签名生成错误');
} else if (body.indexOf('Authorization has expired') === 0) {
return reject('签名已过期');
} else if (body.indexOf('Invalid input') === 0) {
return reject('无数据输入');
}
if (config.UC_CHARSET !== 'utf8') {
body = iconv.decode(body, config.UC_CHARSET);
} else {
body = body.toString();
}
resolve(body);
});
});
function buildPostQS(args) {
args = args || {}; // Empty Arguments
let sep = '';
let s = '';
for (let k in args) {
const key = encodeURIComponent(k || '');
const v = args[k || ''];
if (v !== null && (Array.isArray(v) || typeof v === 'object')) {
let s2 = '';
let sep2 = '';
for (let k2 in v) {
const key2 = urlEncode(k2 || '');
s2 += `${sep2}{${key}}[${key2}]=${urlEncode(v[k2 || ''])}`;
sep2 = '&';
}
s += sep + s2;
} else {
s += `${sep}${key}=${urlEncode(v)}`;
}
sep = '&';
}
return s;
}
}
/**
* Build API Request Data
* @param {string} module
* @param {string} action
* @param {string} arg
* @param {string} extra
* @returns {string}
*/
function ucApiRequestData(module, action, arg, extra) {
const input = ucApiInput(arg);
return `m=${module}&a=${action}&inajax=2&release=${UC_CLIENT_RELEASE}&input=${input}&appid=${config.UC_APPID}${extra || ''}`;
}
/**
* Build API Input param
* @param {*} data
*/
function ucApiInput(data) {
const time = ~~(Date.now() / 1000); // Create Timestamp
const sign = data + '&agent=' + md5(UserAgent) + "&time=" + time; // Build sign plaintext
// console.log('ucApiInput:', sign);
const authCode = ucAuthCode(sign, 'ENCODE', config.UC_KEY); // Build Auth Code
// console.log('authCode:', authCode);
return urlEncode(authCode, 'utf8'); // Do URL Encode
}
/**
* Do UC AuthCode
* @param {*} str
* @param {*} operation ENCODE / DECODE
* @param {*} key
* @param {*} expiry Exipred time
*/
function ucAuthCode(str, operation, key, expiry) {
if (!operation) operation = 'DECODE'; // DEFAULT OP is DECODE
const ckeyLength = 4; // ckey Length
const nowTime = ~~(Date.now() / 1000);
key = md5(key || config.UC_KEY);
const keyA = md5(key.substr(0, 16));
const keyB = md5(key.substr(16, 16));
if (!ckeyLength) return '';
let keyC = operation === 'DECODE' ? str.substr(0, ckeyLength) : randStr(ckeyLength);
const cryptKey = keyA + md5(keyA + keyC);
const keyLength = cryptKey.length;
if (operation === 'DECODE') {
str = Buffer.from(str.substr(ckeyLength), 'base64');
} else {
if (expiry) {
expiry += nowTime;
} else {
expiry = 0;
}
str = Buffer.from('0000000000'.concat(expiry).slice(-10) + md5(str + keyB).substr(0, 16) + str);
}
const strLength = str.length;
const result = Buffer.alloc(strLength);
// Generate SBox
const box = [];
const rndKey = [];
for (let i = 0; i <= 255; i++) {
box[i] = i;
rndKey[i] = cryptKey.charCodeAt(i % keyLength);
}
for (let j = 0, i = 0; i < 256; i++) {
j = (j + box[i] + rndKey[i]) % 256;
const tmp = box[i];
box[i] = box[j];
box[j] = tmp;
}
// Replace PlainText / CipherText
for (let a = 0, j = 0, i = 0; i < strLength; i++) {
a = (a + 1) % 256;
j = (j + box[a]) % 256;
const tmp = box[a];
box[a] = box[j];
box[j] = tmp;
result[i] = str[i] ^ (box[(box[a] + box[j]) % 256]);
}
// Check Decoded Data
if (operation === 'DECODE') {
const finalResult = result.toString();
const expiredTime = parseInt(finalResult.substr(0, 10));
if (expiredTime !== 0 && expiredTime - nowTime > 0) return '';
const sign = Buffer.concat([result.slice(26), Buffer.from(keyB)]);
if (finalResult.substr(10, 16) === md5(sign).substr(0, 16)) {
if (config.UC_CHARSET === 'utf8') {
return finalResult.substr(26);
} else {
return iconv.decode(result.slice(26), config.UC_CHARSET);
}
} else {
return '';
}
} else {
// Format Encoded Data
return keyC + result.toString('base64').replace(/=/g, '');
}
}
};