Some checks failed
Build docker and publish / prepare (20.15.1) (push) Successful in 10s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.admin image_name:ppanel-admin name:admin]) (push) Successful in 1m16s
Build docker and publish / deploy (push) Has been cancelled
Build docker and publish / notify (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.rpc-core image_name:ppanel-rpc-core name:rpc-core]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.scheduler image_name:ppanel-scheduler name:scheduler]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.node image_name:ppanel-node name:node]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.queue image_name:ppanel-queue name:queue]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.api image_name:ppanel-api name:api]) (push) Has been cancelled
149 lines
5.6 KiB
JavaScript
149 lines
5.6 KiB
JavaScript
/**
|
||
* 设备登录接口测试脚本
|
||
*
|
||
* 复现服务端的加密与签名逻辑:
|
||
* - AES-CBC 加密: key=SHA256(secret)[:32], iv=SHA256(md5hex(nonce)+secret)[:16]
|
||
* - 签名: HMAC-SHA256(appSecret, METHOD\nPATH\nQUERY\nBODY_SHA256\nAppId\nTimestamp\nNonce)
|
||
*/
|
||
|
||
import crypto from 'node:crypto';
|
||
import https from 'node:https';
|
||
import http from 'node:http';
|
||
|
||
// ─── 配置(对应 api-dev.yaml)─────────────────────────────────────────────────
|
||
const CONFIG = {
|
||
baseUrl: 'http://127.0.0.1:8080', // 本地测试改这里;生产改为 https://tapi.hifast.biz
|
||
appId: 'android-client',
|
||
appSecret: 'uB4G,XxL2{7b}', // AppSignature.AppSecrets[appId]
|
||
securitySecret: 'uB4G,XxL2{7b', // Security.SecuritySecret(注意少一个 })
|
||
};
|
||
|
||
// ─── AES-CBC 加密工具(与 pkg/cryptox/aes.go 对齐)─────────────────────────────
|
||
|
||
function generateKey(secret) {
|
||
return crypto.createHash('sha256').update(secret).digest().slice(0, 32);
|
||
}
|
||
|
||
function generateIv(nonce, secret) {
|
||
const md5hex = crypto.createHash('md5').update(nonce).digest('hex');
|
||
return crypto.createHash('sha256').update(md5hex + secret).digest().slice(0, 16);
|
||
}
|
||
|
||
function aesEncrypt(plaintext, secret) {
|
||
const nonce = crypto.randomBytes(8).toString('hex'); // 16位 hex,对应 Go 的 fmt.Sprintf("%x", UnixNano)
|
||
const key = generateKey(secret);
|
||
const iv = generateIv(nonce, secret);
|
||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||
return {
|
||
data: encrypted.toString('base64'),
|
||
time: nonce,
|
||
};
|
||
}
|
||
|
||
// ─── 签名工具(与 pkg/signature/canonical.go 对齐)────────────────────────────
|
||
|
||
function sha256Hex(data) {
|
||
return crypto.createHash('sha256').update(data).digest('hex');
|
||
}
|
||
|
||
function buildStringToSign(method, path, rawQuery, bodyBytes, appId, timestamp, nonce) {
|
||
// 规范化 query(按 key 字母排序)
|
||
let canonical = '';
|
||
if (rawQuery) {
|
||
const params = new URLSearchParams(rawQuery);
|
||
const sorted = [...params.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||
canonical = sorted.map(([k, v]) => `${k}=${v}`).join('&');
|
||
}
|
||
const bodyHash = sha256Hex(bodyBytes);
|
||
return [method.toUpperCase(), path, canonical, bodyHash, appId, timestamp, nonce].join('\n');
|
||
}
|
||
|
||
function hmacSha256(secret, data) {
|
||
return crypto.createHmac('sha256', secret).update(data).digest('hex');
|
||
}
|
||
|
||
// ─── 发送请求 ─────────────────────────────────────────────────────────────────
|
||
|
||
async function request(method, path, payload) {
|
||
const url = new URL(CONFIG.baseUrl + path);
|
||
|
||
const timestamp = String(Math.floor(Date.now() / 1000));
|
||
const nonce = crypto.randomBytes(16).toString('hex');
|
||
|
||
// 1. AES 加密 body
|
||
const { data: encData, time: encTime } = aesEncrypt(JSON.stringify(payload), CONFIG.securitySecret);
|
||
const bodyObj = { data: encData, time: encTime };
|
||
const bodyStr = JSON.stringify(bodyObj);
|
||
const bodyBytes = Buffer.from(bodyStr, 'utf8');
|
||
|
||
// 2. 构造签名
|
||
const sts = buildStringToSign(method, url.pathname, url.search.slice(1), bodyBytes, CONFIG.appId, timestamp, nonce);
|
||
const sig = hmacSha256(CONFIG.appSecret, sts);
|
||
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
'X-App-Id': CONFIG.appId,
|
||
'X-Timestamp': timestamp,
|
||
'X-Nonce': nonce,
|
||
'X-Signature': sig,
|
||
'Login-Type': 'device',
|
||
};
|
||
|
||
console.log('\n📤 请求信息');
|
||
console.log(' URL:', url.toString());
|
||
console.log(' timestamp:', timestamp);
|
||
console.log(' nonce:', nonce);
|
||
console.log(' signature:', sig);
|
||
console.log(' 加密 nonce(time):', encTime);
|
||
console.log(' 签名原文:\n' + sts.split('\n').map(l => ' ' + l).join('\n'));
|
||
console.log(' body:', bodyStr);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const lib = url.protocol === 'https:' ? https : http;
|
||
const req = lib.request(url, { method, headers }, (res) => {
|
||
let raw = '';
|
||
res.on('data', chunk => raw += chunk);
|
||
res.on('end', () => {
|
||
console.log('\n📥 响应');
|
||
console.log(' status:', res.statusCode);
|
||
try {
|
||
console.log(' body:', JSON.stringify(JSON.parse(raw), null, 2));
|
||
} catch {
|
||
console.log(' body:', raw);
|
||
}
|
||
resolve({ status: res.statusCode, body: raw });
|
||
});
|
||
});
|
||
req.on('error', reject);
|
||
req.write(bodyStr);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ─── 测试用例 ──────────────────────────────────────────────────────────────────
|
||
|
||
async function main() {
|
||
console.log('='.repeat(60));
|
||
console.log('🔐 设备登录接口测试');
|
||
console.log('='.repeat(60));
|
||
|
||
// 测试 1:正常设备登录
|
||
console.log('\n【TEST 1】正常设备登录');
|
||
await request('POST', '/api/v1/auth/login/device', {
|
||
identifier: 'test-device-001',
|
||
user_agent: 'TestClient/1.0 (Node.js test)',
|
||
short_code: '',
|
||
cf_token: '',
|
||
});
|
||
|
||
// 测试 2:不同 identifier(新设备,应该自动注册)
|
||
console.log('\n【TEST 2】新设备首次登录');
|
||
await request('POST', '/api/v1/auth/login/device', {
|
||
identifier: `device-${Date.now()}`,
|
||
user_agent: 'TestClient/1.0 (Node.js test)',
|
||
});
|
||
}
|
||
|
||
main().catch(console.error);
|