All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m18s
296 lines
8.0 KiB
Go
296 lines
8.0 KiB
Go
package main
|
||
|
||
// 设备登录测试脚本
|
||
// 用法: go run scripts/test_device_login.go
|
||
// 功能: 模拟客户端设备登录,自动加密请求体,解密响应,打印 token
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/hmac"
|
||
"crypto/md5"
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/forgoer/openssl"
|
||
)
|
||
|
||
// ==================== 配置区域 ====================
|
||
|
||
const (
|
||
serverURL = "https://tapi.hifast.biz" // 服务地址
|
||
securitySecret = "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx" // device.security_secret
|
||
identifier = "test-device-script-001" // 设备唯一标识
|
||
userAgent = "TestScript/1.0" // UserAgent
|
||
|
||
appId = "android-client" // AppSignature.AppSecrets 中的 key
|
||
appSecret = "uB4G,XxL2{7b" // AppSignature.AppSecrets 中的 value
|
||
)
|
||
|
||
// ==================== AES 工具(与服务端 pkg/aes/aes.go 一致)====================
|
||
|
||
func generateKey(key string) []byte {
|
||
hash := sha256.Sum256([]byte(key))
|
||
return hash[:32]
|
||
}
|
||
|
||
func generateIv(iv, key string) []byte {
|
||
h := md5.New()
|
||
h.Write([]byte(iv))
|
||
return generateKey(hex.EncodeToString(h.Sum(nil)) + key)
|
||
}
|
||
|
||
func aesEncrypt(plainText []byte, keyStr string) (data string, nonce string, err error) {
|
||
nonce = fmt.Sprintf("%x", time.Now().UnixNano())
|
||
key := generateKey(keyStr)
|
||
iv := generateIv(nonce, keyStr)
|
||
dst, err := openssl.AesCBCEncrypt(plainText, key, iv, openssl.PKCS7_PADDING)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
return base64.StdEncoding.EncodeToString(dst), nonce, nil
|
||
}
|
||
|
||
func aesDecrypt(cipherText string, keyStr string, ivStr string) (string, error) {
|
||
decode, err := base64.StdEncoding.DecodeString(cipherText)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
key := generateKey(keyStr)
|
||
iv := generateIv(ivStr, keyStr)
|
||
dst, err := openssl.AesCBCDecrypt(decode, key, iv, openssl.PKCS7_PADDING)
|
||
return string(dst), err
|
||
}
|
||
|
||
// ==================== 签名工具(与服务端 pkg/signature 一致)====================
|
||
|
||
func buildStringToSign(method, path, rawQuery string, body []byte, xAppId, timestamp, nonce string) string {
|
||
canonical := canonicalQuery(rawQuery)
|
||
bodyHash := sha256Hex(body)
|
||
parts := []string{
|
||
strings.ToUpper(method),
|
||
path,
|
||
canonical,
|
||
bodyHash,
|
||
xAppId,
|
||
timestamp,
|
||
nonce,
|
||
}
|
||
return strings.Join(parts, "\n")
|
||
}
|
||
|
||
func canonicalQuery(rawQuery string) string {
|
||
if rawQuery == "" {
|
||
return ""
|
||
}
|
||
pairs := strings.Split(rawQuery, "&")
|
||
sort.Strings(pairs)
|
||
return strings.Join(pairs, "&")
|
||
}
|
||
|
||
func sha256Hex(data []byte) string {
|
||
h := sha256.Sum256(data)
|
||
return fmt.Sprintf("%x", h)
|
||
}
|
||
|
||
func buildSignature(stringToSign, secret string) string {
|
||
mac := hmac.New(sha256.New, []byte(secret))
|
||
mac.Write([]byte(stringToSign))
|
||
return hex.EncodeToString(mac.Sum(nil))
|
||
}
|
||
|
||
func signedRequest(method, url, rawQuery string, body []byte, token string) (*http.Request, error) {
|
||
var bodyReader io.Reader
|
||
if body != nil {
|
||
bodyReader = bytes.NewReader(body)
|
||
}
|
||
req, err := http.NewRequest(method, url, bodyReader)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
if token != "" {
|
||
req.Header.Set("Authorization", token) // 不带 Bearer 前缀,服务端直接 Parse token
|
||
}
|
||
|
||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||
nonce := fmt.Sprintf("%x", time.Now().UnixNano())
|
||
|
||
// 提取 path
|
||
path := req.URL.Path
|
||
|
||
sts := buildStringToSign(method, path, rawQuery, body, appId, timestamp, nonce)
|
||
sig := buildSignature(sts, appSecret)
|
||
|
||
req.Header.Set("X-App-Id", appId)
|
||
req.Header.Set("X-Timestamp", timestamp)
|
||
req.Header.Set("X-Nonce", nonce)
|
||
req.Header.Set("X-Signature", sig)
|
||
return req, nil
|
||
}
|
||
|
||
// ==================== 主逻辑 ====================
|
||
|
||
func main() {
|
||
fmt.Println("=== 设备登录测试 ===")
|
||
fmt.Printf("Server: %s\n", serverURL)
|
||
fmt.Printf("Identifier: %s\n", identifier)
|
||
fmt.Println()
|
||
|
||
// 1. 构造原始请求体
|
||
payload := map[string]string{
|
||
"identifier": identifier,
|
||
"user_agent": userAgent,
|
||
}
|
||
plainBytes, err := json.Marshal(payload)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] marshal payload: %v\n", err)
|
||
return
|
||
}
|
||
fmt.Printf("原始请求体: %s\n", string(plainBytes))
|
||
|
||
// 2. AES 加密请求体
|
||
encData, nonce, err := aesEncrypt(plainBytes, securitySecret)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] encrypt: %v\n", err)
|
||
return
|
||
}
|
||
|
||
encBody := map[string]string{
|
||
"data": encData,
|
||
"time": nonce,
|
||
}
|
||
encBytes, _ := json.Marshal(encBody)
|
||
fmt.Printf("加密请求体: %s\n\n", string(encBytes))
|
||
|
||
// 3. 发送请求
|
||
req, err := signedRequest("POST", serverURL+"/v1/auth/login/device", "", encBytes, "")
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] new request: %v\n", err)
|
||
return
|
||
}
|
||
req.Header.Set("Login-Type", "device")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] request failed: %v\n", err)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
fmt.Printf("HTTP Status: %d\n", resp.StatusCode)
|
||
fmt.Printf("原始响应: %s\n\n", string(respBody))
|
||
|
||
// 4. 解密响应
|
||
// 响应格式: {"code":200,"data":{"data":"<encrypted>","time":"<nonce>"},"message":""}
|
||
var outer struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
if err := json.Unmarshal(respBody, &outer); err != nil {
|
||
fmt.Printf("[ERROR] parse response: %v\n", err)
|
||
return
|
||
}
|
||
|
||
if outer.Code != 200 {
|
||
fmt.Printf("[FAIL] 登录失败: code=%d message=%s\n", outer.Code, outer.Message)
|
||
return
|
||
}
|
||
|
||
// data 字段是加密对象
|
||
var encResp struct {
|
||
Data string `json:"data"`
|
||
Time string `json:"time"`
|
||
}
|
||
if err := json.Unmarshal(outer.Data, &encResp); err != nil {
|
||
// 如果 Device.Enable=false,data 直接就是明文对象
|
||
fmt.Printf("响应 data 非加密格式,直接解析: %s\n", string(outer.Data))
|
||
var loginResp struct {
|
||
Token string `json:"token"`
|
||
}
|
||
if err2 := json.Unmarshal(outer.Data, &loginResp); err2 == nil && loginResp.Token != "" {
|
||
fmt.Printf("[OK] Token: %s\n", loginResp.Token)
|
||
}
|
||
return
|
||
}
|
||
|
||
decrypted, err := aesDecrypt(encResp.Data, securitySecret, encResp.Time)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] decrypt response: %v\n", err)
|
||
return
|
||
}
|
||
fmt.Printf("解密后响应: %s\n\n", decrypted)
|
||
|
||
var loginResp struct {
|
||
Token string `json:"token"`
|
||
}
|
||
if err := json.Unmarshal([]byte(decrypted), &loginResp); err != nil {
|
||
fmt.Printf("[ERROR] parse decrypted: %v\n", err)
|
||
return
|
||
}
|
||
|
||
fmt.Printf("[OK] Token: %s\n", loginResp.Token)
|
||
|
||
// 5. 用 token 请求订阅列表
|
||
fmt.Println("\n=== 请求订阅列表 ===")
|
||
subReq, err := signedRequest("GET", serverURL+"/v1/public/subscribe/list", "", nil, loginResp.Token)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] build subscribe request: %v\n", err)
|
||
return
|
||
}
|
||
subReq.Header.Set("Login-Type", "device")
|
||
|
||
subResp, err := client.Do(subReq)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] subscribe list request: %v\n", err)
|
||
return
|
||
}
|
||
defer subResp.Body.Close()
|
||
|
||
subBody, _ := io.ReadAll(subResp.Body)
|
||
fmt.Printf("HTTP Status: %d\n", subResp.StatusCode)
|
||
fmt.Printf("原始响应: %s\n", string(subBody))
|
||
|
||
// 解密订阅列表响应
|
||
var subOuter struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
if err := json.Unmarshal(subBody, &subOuter); err != nil {
|
||
fmt.Printf("[ERROR] parse subscribe response: %v\n", err)
|
||
return
|
||
}
|
||
if subOuter.Code != 200 {
|
||
fmt.Printf("[FAIL] 订阅列表失败: code=%d message=%s\n", subOuter.Code, subOuter.Message)
|
||
return
|
||
}
|
||
|
||
var subEnc struct {
|
||
Data string `json:"data"`
|
||
Time string `json:"time"`
|
||
}
|
||
if err := json.Unmarshal(subOuter.Data, &subEnc); err != nil || subEnc.Data == "" {
|
||
// 无加密,直接打印
|
||
fmt.Printf("\n[OK] 订阅列表(明文): %s\n", string(subOuter.Data))
|
||
return
|
||
}
|
||
subDecrypted, err := aesDecrypt(subEnc.Data, securitySecret, subEnc.Time)
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] decrypt subscribe list: %v\n", err)
|
||
return
|
||
}
|
||
fmt.Printf("\n[OK] 订阅列表(解密): %s\n", subDecrypted)
|
||
}
|