From ed4a56170496a9e2f8cdef433c8da78d2e845951 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 28 May 2026 11:58:44 +0800 Subject: [PATCH] feat: add application API documentation and C++ SDK - Add complete API documentation for application integration (docs/API_DOCUMENT.md) - Fix check-update API to use version ID comparison instead of string comparison - Add validation: client version must exist in server version list - Add support for patch/incremental updates with base_version check - Add C++ SDK with HTTP client, JSON parser, and crypto support - Add simple_test.cpp for standalone testing on Windows Co-Authored-By: Claude Opus 4.7 --- backend/internal/router/app/info.go | 127 +++- docs/API_DOCUMENT.md | 876 ++++++++++++++++++++++++++++ sdk/cpp/CMakeLists.txt | 62 ++ sdk/cpp/README.md | 141 +++++ sdk/cpp/json.hpp | 353 +++++++++++ sdk/cpp/simple_test.cpp | 447 ++++++++++++++ sdk/cpp/test_main.cpp | 361 ++++++++++++ sdk/cpp/vcpkg.json | 10 + sdk/cpp/verify_client.cpp | 830 ++++++++++++++++++++++++++ sdk/cpp/verify_client.hpp | 230 ++++++++ 10 files changed, 3423 insertions(+), 14 deletions(-) create mode 100644 docs/API_DOCUMENT.md create mode 100644 sdk/cpp/CMakeLists.txt create mode 100644 sdk/cpp/README.md create mode 100644 sdk/cpp/json.hpp create mode 100644 sdk/cpp/simple_test.cpp create mode 100644 sdk/cpp/test_main.cpp create mode 100644 sdk/cpp/vcpkg.json create mode 100644 sdk/cpp/verify_client.cpp create mode 100644 sdk/cpp/verify_client.hpp diff --git a/backend/internal/router/app/info.go b/backend/internal/router/app/info.go index 63ad420..45c2ba6 100644 --- a/backend/internal/router/app/info.go +++ b/backend/internal/router/app/info.go @@ -48,6 +48,8 @@ func handleAppGetInfo(c *gin.Context) { func handleAppCheckUpdate(c *gin.Context) { appKey := c.Param("appKey") + clientVersion := c.Query("version") + var app model.Application if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil { response.Error(c, 404, "应用不存在") @@ -59,14 +61,21 @@ func handleAppCheckUpdate(c *gin.Context) { return } - clientVersion := c.Query("version") + // 查询该应用的所有版本,按ID降序(创建顺序) + var allVersions []model.Version + if err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active"). + Order("id DESC"). + Preload("Files"). + Find(&allVersions).Error; err != nil { + response.Error(c, 500, "获取版本列表失败") + return + } - var latestVersion model.Version - err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("created_at DESC").Preload("Files").First(&latestVersion).Error - - if err != nil { + // 没有任何版本 + if len(allVersions) == 0 { response.Success(c, gin.H{ "has_update": false, + "current_version": clientVersion, "latest_version": "", "download_url": "", "update_notes": "", @@ -77,14 +86,82 @@ func handleAppCheckUpdate(c *gin.Context) { return } - hasUpdate := clientVersion != latestVersion.Version - if latestVersion.UpdateStrategy == "forced" { - hasUpdate = true + // 客户端必须提供版本号 + if clientVersion == "" { + response.Error(c, 400, "请提供客户端版本号") + return } - files := make([]gin.H, len(latestVersion.Files)) + // 找到客户端当前版本对应的版本记录 + var clientVersionRecord *model.Version + for i := range allVersions { + if allVersions[i].Version == clientVersion { + clientVersionRecord = &allVersions[i] + break + } + } + + // 客户端版本不存在于服务器版本列表中,返回错误 + if clientVersionRecord == nil { + response.Error(c, 404, "客户端版本不存在") + return + } + + // 判断客户端版本状态:检查是否有更高ID的强制更新版本 + isSuperseded := false + for i := range allVersions { + v := &allVersions[i] + // 存在ID更高(创建时间更晚)的强制更新版本 + if v.ID > clientVersionRecord.ID && v.UpdateStrategy == "forced" { + isSuperseded = true + break + } + } + + // 如果被取代(有强制更新),必须更新 + // 找到最新的强制更新版本 + var latestVersion *model.Version + var updateStrategy string + + if isSuperseded { + // 找到ID最高的强制更新版本 + for i := range allVersions { + v := &allVersions[i] + if v.ID > clientVersionRecord.ID && v.UpdateStrategy == "forced" { + if latestVersion == nil || v.ID > latestVersion.ID { + latestVersion = v + } + } + } + updateStrategy = "forced" + } else { + // 没有强制更新,检查是否有更高ID的版本(普通更新) + // 最新版本就是ID最高的版本(已经是按ID降序排列,第一个就是最新) + latestVersion = &allVersions[0] + + // 只有当最新版本ID大于客户端版本ID时才需要更新 + if latestVersion.ID > clientVersionRecord.ID { + updateStrategy = latestVersion.UpdateStrategy // optional 或 forced + } else { + // 已经是最新版本 + response.Success(c, gin.H{ + "has_update": false, + "current_version": clientVersion, + "latest_version": clientVersionRecord.Version, + "download_url": "", + "update_notes": "", + "update_strategy": "", + "update_method": "", + "files": []interface{}{}, + }) + return + } + } + + // 构建文件列表 + files := make([]map[string]interface{}, len(latestVersion.Files)) for i, f := range latestVersion.Files { - files[i] = gin.H{ + files[i] = map[string]interface{}{ "file_path": f.FilePath, "file_name": f.FileName, "file_size": f.FileSize, @@ -94,20 +171,42 @@ func handleAppCheckUpdate(c *gin.Context) { } } - response.Success(c, gin.H{ - "has_update": hasUpdate, + // 构建返回结果 + result := map[string]interface{}{ + "has_update": true, + "current_version": clientVersion, "latest_version": latestVersion.Version, "download_url": latestVersion.FilePath, "file_size": latestVersion.FileSize, "file_hash": latestVersion.FileHash, "entry_file": latestVersion.EntryFile, "update_notes": latestVersion.Description, - "update_strategy": latestVersion.UpdateStrategy, + "update_strategy": updateStrategy, "update_type": latestVersion.UpdateType, "update_method": latestVersion.UpdateMethod, "changelog": latestVersion.Changelog, "files": files, - }) + } + + // 检查是否有适合客户端版本的增量更新 + // 查找以客户端版本为基础版本的 patch 更新 + var patchVersion model.Version + err := database.DB.Where("application_id = ? AND update_type = ? AND base_version_id = ? AND status = ?", + app.ID, "patch", clientVersionRecord.ID, "active"). + Order("id DESC"). + Preload("Files"). + First(&patchVersion).Error + + if err == nil { + // 找到了增量更新 + result["is_patch"] = true + result["base_version"] = clientVersion + result["patch_url"] = patchVersion.FilePath + result["patch_size"] = patchVersion.FileSize + result["patch_hash"] = patchVersion.FileHash + } + + response.Success(c, result) } func handleAppGetAnnouncements(c *gin.Context) { diff --git a/docs/API_DOCUMENT.md b/docs/API_DOCUMENT.md new file mode 100644 index 0000000..b944ef5 --- /dev/null +++ b/docs/API_DOCUMENT.md @@ -0,0 +1,876 @@ +# 应用对接API文档 + +## 概述 + +本文档描述了验证平台应用对接API的详细说明,供第三方应用开发者集成使用。 + +**服务器地址**: `https://gendan.xyz` +**API基础路径**: `/api/v1/app/{appKey}` + +--- + +## 认证与加密 + +### 请求认证 + +- 公开接口:无需认证 +- 需认证接口:请求头携带 `Authorization: Bearer {token}` +- Token通过登录接口获取 + +### 数据加密 + +平台支持AES和RC4两种加密方式,具体加密类型由应用配置决定。 + +#### AES加密 + +- 算法:AES-GCM +- 密钥长度:16/24/32字节(自动填充至32字节) +- 格式:Base64编码(nonce + ciphertext) + +#### RC4加密 + +- 密钥长度:任意长度 +- 格式:Base64编码 + +--- + +## API接口列表 + +### 1. 应用信息接口(公开) + +#### 1.1 获取应用信息 + +**请求** +``` +GET /api/v1/app/{appKey}/info +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "name": "应用名称", + "description": "应用描述", + "icon_url": "图标URL", + "status": "active", + "billing_type": "balance", + "login_policy": "strict", + "max_devices": 1, + "multi_open_mode": "forbidden", + "max_instances": 1, + "enable_trial": false, + "trial_balance": 0, + "heartbeat_interval": 60, + "heartbeat_timeout": 300 + } +} +``` + +#### 1.2 检查更新 + +**请求** +``` +GET /api/v1/app/{appKey}/check-update?version=1.0.0 +``` + +**参数说明** +- `version` (必填): 客户端当前版本号,必须存在于服务器版本列表中 + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "has_update": true, + "current_version": "1.0.0", + "latest_version": "1.0.1", + "download_url": "下载地址", + "file_size": 1024000, + "file_hash": "文件哈希", + "entry_file": "入口文件", + "update_notes": "更新说明", + "update_strategy": "optional|forced", + "update_type": "full|patch", + "update_method": "auto|manual", + "changelog": "更新日志", + "files": [], + "is_patch": false + } +} +``` + +**错误响应** +```json +{ + "code": 400, + "message": "请提供客户端版本号" +} +``` + +```json +{ + "code": 404, + "message": "客户端版本不存在" +} +``` + +**更新判断逻辑** + +基于版本ID(创建顺序)判断,与版本管理页面一致: + +| has_update | update_strategy | 含义 | +|------------|-----------------|------| +| false | "" | 已是最新版本,无需更新 | +| true | optional | 有可选更新 | +| true | forced | 必须强制更新(存在更高ID的强制版本) | + +**判断规则** +1. 如果存在 `ID > 客户端版本ID` 且 `update_strategy = "forced"` 的版本 → `has_update=true, update_strategy="forced"` +2. 如果存在 `ID > 客户端版本ID` 的版本(无强制更新) → `has_update=true, update_strategy="optional"` +3. 客户端版本ID已是最高 → `has_update=false` + +**增量更新** +- 如果 `update_type` 为 `"patch"` 且存在以客户端版本为基础的增量包: + - `is_patch`: true + - `base_version`: 基础版本号 + - `patch_url`: 增量包下载地址 + - `patch_size`: 增量包大小 + - `patch_hash`: 增量包哈希 + +#### 1.3 获取公告列表 + +**请求** +``` +GET /api/v1/app/{appKey}/announcements +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "title": "公告标题", + "content": "公告内容", + "type": "info", + "is_top": true, + "created_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +--- + +### 2. 用户认证接口(公开) + +#### 2.1 用户注册 + +**请求** +``` +POST /api/v1/app/{appKey}/register +Content-Type: application/json + +{ + "username": "用户名", + "email": "邮箱(如需验证)", + "email_code": "邮箱验证码(如需验证)", + "phone": "手机号(如需短信验证)", + "sms_code": "短信验证码(如需短信验证)", + "password": "密码", + "device_id": "设备ID", + "device_name": "设备名称", + "device_type": "android|ios|windows|mac|linux|web", + "instance_id": "实例ID" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "注册成功", + "data": { + "user_id": 1 + } +} +``` + +#### 2.2 用户登录 + +**请求** +``` +POST /api/v1/app/{appKey}/login +Content-Type: application/json + +{ + "username": "用户名", + "password": "密码", + "device_id": "设备ID(必填)", + "device_name": "设备名称", + "device_type": "android|ios|windows|mac|linux|web", + "instance_id": "实例ID" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "user_id": 1, + "token": "JWT令牌" + } +} +``` + +**错误响应** +```json +{ + "code": 403, + "message": "设备绑定数量已达上限,请解绑后再试", + "data": { + "error_code": "DEVICE_LIMIT_EXCEEDED", + "max_devices": 1, + "device_count": 1, + "devices": [ + { + "id": 1, + "device_id": "xxx", + "device_name": "设备名", + "device_type": "windows", + "online_count": 0, + "created_at": "2024-01-01T00:00:00Z" + } + ] + } +} +``` + +#### 2.3 发送邮箱验证码 + +**请求** +``` +POST /api/v1/app/{appKey}/send-email-code +Content-Type: application/json + +{ + "email": "邮箱地址", + "purpose": "register|reset_password" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "验证码已发送" + } +} +``` + +#### 2.4 重置密码 + +**请求** +``` +POST /api/v1/app/{appKey}/reset-password +Content-Type: application/json + +{ + "email": "邮箱地址", + "code": "验证码", + "password": "新密码(至少6位)" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "密码重置成功" + } +} +``` + +#### 2.5 修改密码 + +**请求** +``` +POST /api/v1/app/{appKey}/change-password +Content-Type: application/json + +{ + "username": "用户名", + "old_password": "原密码", + "new_password": "新密码(至少6位)" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "密码修改成功" + } +} +``` + +--- + +### 3. 账户接口(需认证) + +#### 3.1 获取账户信息 + +**请求** +``` +POST /api/v1/app/{appKey}/account +Authorization: Bearer {token} +Content-Type: application/json + +{ + "user_id": 1 +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "user_id": 1, + "username": "用户名", + "balance": 100.0, + "status": "active" + } +} +``` + +#### 3.2 心跳上报 + +**请求** +``` +POST /api/v1/app/{appKey}/heartbeat +Authorization: Bearer {token} +Content-Type: application/json + +{ + "user_id": 1, + "device_id": "设备ID", + "instance_id": "实例ID" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "心跳成功", + "data": { + "message": "心跳成功", + "balance": 99.0 + } +} +``` + +--- + +### 4. 充值接口(公开) + +#### 4.1 卡密充值 + +**请求** +``` +POST /api/v1/app/{appKey}/recharge +Content-Type: application/json + +{ + "username": "用户名", + "card_key": "卡密", + "device_id": "设备ID" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "充值成功", + "data": { + "message": "充值成功", + "value": 30.0 + } +} +``` + +#### 4.2 试用 + +**请求** +``` +POST /api/v1/app/{appKey}/trial +Content-Type: application/json + +{ + "user_id": 1 +} +``` + +**响应** +```json +{ + "code": 200, + "message": "试用成功", + "data": { + "message": "试用成功", + "trial_balance": 10.0 + } +} +``` + +--- + +### 5. 设备管理接口(需认证) + +#### 5.1 获取设备列表 + +**请求** +``` +GET /api/v1/app/{appKey}/devices +Authorization: Bearer {token} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "device_id": "设备ID", + "device_name": "设备名称", + "device_type": "windows", + "status": "active", + "online_sessions": 1, + "created_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +#### 5.2 获取设备数量 + +**请求** +``` +POST /api/v1/app/{appKey}/device-count +Authorization: Bearer {token} +Content-Type: application/json + +{ + "user_id": 1 +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "count": 1, + "max_devices": 1, + "remaining": 0 + } +} +``` + +#### 5.3 解绑设备(需认证) + +**请求** +``` +POST /api/v1/app/{appKey}/unbind-device +Authorization: Bearer {token} +Content-Type: application/json + +{ + "user_id": 1, + "device_id": "设备ID" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "解绑成功" + } +} +``` + +#### 5.4 解绑设备(用户认证) + +**请求** +``` +POST /api/v1/app/{appKey}/unbind-device-with-auth +Content-Type: application/json + +{ + "username": "用户名", + "password": "密码", + "device_id": "设备ID" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "解绑成功" + } +} +``` + +#### 5.5 获取实例列表 + +**请求** +``` +POST /api/v1/app/{appKey}/instances +Authorization: Bearer {token} +Content-Type: application/json + +{ + "user_id": 1 +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "instance_id": "实例ID", + "device_id": "设备ID", + "device_name": "设备名称", + "is_online": true, + "last_heartbeat": "2024-01-01T00:00:00Z", + "created_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +#### 5.6 强制下线实例 + +**请求** +``` +POST /api/v1/app/{appKey}/instances/{instance_id}/offline +Authorization: Bearer {token} +Content-Type: application/json + +{ + "user_id": 1 +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "已强制离线" + } +} +``` + +--- + +### 6. 云端数据接口(需认证) + +#### 6.1 获取云端常量列表 + +**请求** +``` +GET /api/v1/app/{appKey}/constants +Authorization: Bearer {token} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "config_key": { + "type": "string", + "value": "配置值" + }, + "file_key": { + "type": "binary", + "value": "/api/v1/app/{appKey}/constants/file_key/download", + "file_name": "文件名", + "file_size": 1024, + "md5": "文件MD5", + "mime_type": "application/octet-stream" + } + } +} +``` + +#### 6.2 获取单个云端常量 + +**请求** +``` +GET /api/v1/app/{appKey}/constants/{key} +Authorization: Bearer {token} +``` + +#### 6.3 下载云端常量文件 + +**请求** +``` +GET /api/v1/app/{appKey}/constants/{key}/download +Authorization: Bearer {token} +``` + +#### 6.4 获取云端变量列表 + +**请求** +``` +GET /api/v1/app/{appKey}/variables +Authorization: Bearer {token} +``` + +#### 6.5 获取单个云端变量 + +**请求** +``` +GET /api/v1/app/{appKey}/variables/{key} +Authorization: Bearer {token} +``` + +#### 6.6 下载云端变量文件 + +**请求** +``` +GET /api/v1/app/{appKey}/variables/{key}/download +Authorization: Bearer {token} +``` + +#### 6.7 上传云端变量文件 + +**请求** +``` +POST /api/v1/app/{appKey}/variables/{key}/upload +Authorization: Bearer {token} +Content-Type: multipart/form-data + +file: 文件内容 +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "message": "上传成功", + "file_url": "文件URL", + "file_size": 1024, + "mime_type": "application/octet-stream", + "original_name": "原文件名", + "download_url": "下载URL" + } +} +``` + +#### 6.8 更新云端变量 + +**请求** +``` +POST /api/v1/app/{appKey}/variables +Authorization: Bearer {token} +Content-Type: application/json + +{ + "variables": { + "key1": "value1", + "key2": "value2" + } +} +``` + +#### 6.9 创建变量记录(Stream类型) + +**请求** +``` +POST /api/v1/app/{appKey}/variables/{key}/records +Authorization: Bearer {token} +Content-Type: application/json + +{ + "field1": "value1", + "field2": "value2" +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "created_at": "2024-01-01T00:00:00Z" + } +} +``` + +#### 6.10 获取变量记录列表 + +**请求** +``` +GET /api/v1/app/{appKey}/variables/{key}/records?page=1&page_size=20 +Authorization: Bearer {token} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "data": {"field": "value"}, + "created_at": "2024-01-01T00:00:00Z" + } + ], + "total": 100, + "page": 1, + "page_size": 20, + "total_pages": 5 + } +} +``` + +#### 6.11 删除变量记录 + +**请求** +``` +DELETE /api/v1/app/{appKey}/variables/{key}/records/{record_id} +Authorization: Bearer {token} +``` + +--- + +### 7. 动态代码接口(需认证) + +#### 7.1 执行动态代码 + +**请求** +``` +POST /api/v1/app/{appKey}/dynamic-code/{key}/execute +Authorization: Bearer {token} +Content-Type: application/json + +{ + "params": { + "param1": "value1" + }, + "user_id": 1 +} +``` + +**响应** +```json +{ + "code": 200, + "message": "success", + "data": { + "result": "执行结果", + "execution_time": 10 + } +} +``` + +--- + +## 错误码说明 + +| 错误码 | 说明 | +|-------|------| +| 200 | 成功 | +| 400 | 参数错误 | +| 401 | 未授权/Token无效 | +| 403 | 禁止访问/余额不足/设备限制等 | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | + +## 特殊错误码 + +| 错误码 | 说明 | +|-------|------| +| DEVICE_LIMIT_EXCEEDED | 设备绑定数量已达上限 | +| IP_LIMIT_EXCEEDED | IP绑定数量已达上限 | +| MULTI_INSTANCE_LIMIT_EXCEEDED | 多开数量已达上限 | + +--- + +## 集成流程 + +1. 调用 `/info` 获取应用配置信息 +2. 调用 `/register` 或 `/login` 获取用户Token +3. 使用Token调用需认证的接口 +4. 定期调用 `/heartbeat` 保持在线状态 +5. 根据需要调用其他接口 + +--- + +## C++ SDK使用示例 + +```cpp +#include "verify_client.hpp" + +int main() { + // 创建客户端 + verify::Client client("https://gendan.xyz", "your_app_key"); + + // 获取应用信息 + auto info = client.getAppInfo(); + + // 用户登录 + auto loginResult = client.login("username", "password", "device_id"); + std::string token = loginResult["token"].asString(); + client.setToken(token); + + // 心跳 + client.heartbeat("user_id", "device_id", "instance_id"); + + // 充值 + client.recharge("username", "card_key", "device_id"); + + return 0; +} +``` diff --git a/sdk/cpp/CMakeLists.txt b/sdk/cpp/CMakeLists.txt new file mode 100644 index 0000000..968cf9c --- /dev/null +++ b/sdk/cpp/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.10) +project(verify_sdk VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 选项 +option(USE_OPENSSL "Enable OpenSSL for AES encryption" ON) +option(BUILD_TESTS "Build test executable" ON) + +# 查找依赖 +find_package(CURL REQUIRED) +find_package(nlohmann_json 3.0.0 REQUIRED) + +if(USE_OPENSSL) + find_package(OpenSSL REQUIRED) + add_compile_definitions(USE_OPENSSL) +endif() + +# 库头文件 +set(HEADERS + verify_client.hpp +) + +# 库源文件 +set(SOURCES + verify_client.cpp +) + +# 创建静态库 +add_library(verify_sdk STATIC ${HEADERS} ${SOURCES}) + +target_include_directories(verify_sdk PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(verify_sdk PUBLIC + CURL::libcurl + nlohmann_json::nlohmann_json +) + +if(USE_OPENSSL) + target_link_libraries(verify_sdk PUBLIC OpenSSL::SSL OpenSSL::Crypto) +endif() + +# 测试可执行文件 +if(BUILD_TESTS) + add_executable(verify_test test_main.cpp) + target_link_libraries(verify_test PRIVATE verify_sdk) +endif() + +# 安装 +install(FILES ${HEADERS} DESTINATION include/verify) +install(TARGETS verify_sdk ARCHIVE DESTINATION lib) + +# 输出信息 +message(STATUS "") +message(STATUS "Verify SDK Configuration:") +message(STATUS " Version: ${PROJECT_VERSION}") +message(STATUS " OpenSSL: ${USE_OPENSSL}") +message(STATUS " Build tests: ${BUILD_TESTS}") +message(STATUS "") diff --git a/sdk/cpp/README.md b/sdk/cpp/README.md new file mode 100644 index 0000000..dc8128b --- /dev/null +++ b/sdk/cpp/README.md @@ -0,0 +1,141 @@ +# 验证平台 C++ SDK + +用于对接验证平台的C++ SDK库。 + +## 依赖 + +- C++17 或更高版本 +- libcurl +- nlohmann/json +- OpenSSL (可选,用于AES加密) + +## 编译 + +### 使用 CMake + +```bash +mkdir build +cd build +cmake .. +make +``` + +### 编译选项 + +- `USE_OPENSSL=ON/OFF` - 启用/禁用 OpenSSL (默认: ON) +- `BUILD_TESTS=ON/OFF` - 构建/不构建测试程序 (默认: ON) + +### Ubuntu/Debian 依赖安装 + +```bash +sudo apt-get install libcurl4-openssl-dev libssl-dev nlohmann-json3-dev cmake g++ +``` + +### Windows (vcpkg) + +```bash +vcpkg install curl nlohmann-json openssl +cmake -DCMAKE_TOOLCHAIN_FILE=[vcpkg路径]/scripts/buildsystems/vcpkg.cmake .. +``` + +## 使用示例 + +### 基本用法 + +```cpp +#include "verify_client.hpp" + +int main() { + // 创建客户端 + verify::Client client("https://gendan.xyz", "your_app_key"); + + // 获取应用信息 + auto info = client.getAppInfo(); + std::cout << "应用名称: " << info.name << std::endl; + + // 用户登录 + auto loginResult = client.login("username", "password", "device_id"); + std::string token = loginResult["token"].get(); + std::cout << "Token: " << token << std::endl; + + // 心跳上报 + client.heartbeat(loginResult["user_id"].get(), "device_id", "instance_id"); + + return 0; +} +``` + +### 使用加密 + +```cpp +// 设置AES加密 +client.setEncryptType(verify::EncryptType::AES, "your_secret_key_32_bytes_long"); + +// 设置RC4加密 +client.setEncryptType(verify::EncryptType::RC4, "your_secret_key"); +``` + +### 设备管理 + +```cpp +// 获取设备列表 +auto devices = client.getDevices(); +for (const auto& d : devices) { + std::cout << "设备: " << d.deviceName << std::endl; +} + +// 解绑设备 +client.unbindDevice(userId, "device_id_to_unbind"); +``` + +### 云端数据 + +```cpp +// 获取云端变量 +auto variables = client.getVariables(); +std::cout << "变量值: " << variables["key"]["value"] << std::endl; + +// 更新云端变量 +std::map vars = {{"key1", "value1"}, {"key2", "value2"}}; +client.updateVariables(vars); + +// 创建变量记录 +json record = {{"field1", "value1"}, {"field2", 123}}; +client.createVariableRecord("stream_key", record); +``` + +## API 参考 + +### Client 类 + +| 方法 | 说明 | +|------|------| +| `getAppInfo()` | 获取应用信息 | +| `checkUpdate(version)` | 检查更新 | +| `getAnnouncements()` | 获取公告列表 | +| `registerUser(...)` | 用户注册 | +| `login(username, password, deviceId)` | 用户登录 | +| `getAccount(userId)` | 获取账户信息 | +| `heartbeat(userId, deviceId, instanceId)` | 心跳上报 | +| `recharge(username, cardKey, deviceId)` | 卡密充值 | +| `getDevices()` | 获取设备列表 | +| `unbindDevice(userId, deviceId)` | 解绑设备 | +| `getConstants()` | 获取云端常量 | +| `getVariables()` | 获取云端变量 | +| `updateVariables(vars)` | 更新云端变量 | +| `executeDynamicCode(key, params)` | 执行动态代码 | + +## 运行测试 + +```bash +./verify_test +``` + +测试选项: +- `--help` 显示帮助 +- `--crypto` 仅测试加密 +- `--perf` 仅测试性能 + +## 许可证 + +MIT License diff --git a/sdk/cpp/json.hpp b/sdk/cpp/json.hpp new file mode 100644 index 0000000..6b31abb --- /dev/null +++ b/sdk/cpp/json.hpp @@ -0,0 +1,353 @@ +// 简化版JSON库 - 仅包含测试所需功能 +#ifndef SIMPLE_JSON_HPP +#define SIMPLE_JSON_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace simple { + +class json { +public: + enum class type_t { + null, + boolean, + number, + string, + array, + object + }; + + json() : type_(type_t::null) {} + json(std::nullptr_t) : type_(type_t::null) {} + json(bool b) : type_(type_t::boolean), bool_val_(b) {} + json(int n) : type_(type_t::number), num_val_(n) {} + json(int64_t n) : type_(type_t::number), num_val_(static_cast(n)) {} + json(uint64_t n) : type_(type_t::number), num_val_(static_cast(n)) {} + json(double n) : type_(type_t::number), num_val_(n) {} + json(const char* s) : type_(type_t::string), str_val_(s) {} + json(const std::string& s) : type_(type_t::string), str_val_(s) {} + + type_t type() const { return type_; } + bool is_null() const { return type_ == type_t::null; } + bool is_bool() const { return type_ == type_t::boolean; } + bool is_number() const { return type_ == type_t::number; } + bool is_string() const { return type_ == type_t::string; } + bool is_array() const { return type_ == type_t::array; } + bool is_object() const { return type_ == type_t::object; } + + size_t size() const { + if (type_ == type_t::array) return arr_val_.size(); + if (type_ == type_t::object) return obj_val_.size(); + return 0; + } + + // Array access + json& operator[](size_t idx) { + if (type_ != type_t::array) { + type_ = type_t::array; + arr_val_.clear(); + } + if (idx >= arr_val_.size()) arr_val_.resize(idx + 1); + return arr_val_[idx]; + } + + const json& operator[](size_t idx) const { + static json null_val; + if (type_ != type_t::array || idx >= arr_val_.size()) return null_val; + return arr_val_[idx]; + } + + // Object access + json& operator[](const std::string& key) { + if (type_ != type_t::object) { + type_ = type_t::object; + obj_val_.clear(); + } + return obj_val_[key]; + } + + const json& operator[](const std::string& key) const { + static json null_val; + if (type_ != type_t::object) return null_val; + auto it = obj_val_.find(key); + return (it != obj_val_.end()) ? it->second : null_val; + } + + // const char* overload to avoid ambiguity + json& operator[](const char* key) { + return (*this)[std::string(key)]; + } + + const json& operator[](const char* key) const { + return (*this)[std::string(key)]; + } + + bool contains(const std::string& key) const { + return type_ == type_t::object && obj_val_.find(key) != obj_val_.end(); + } + + // Value getters + bool get_bool() const { return bool_val_; } + double get_double() const { return num_val_; } + int64_t get_int64() const { return static_cast(num_val_); } + uint64_t get_uint64() const { return static_cast(num_val_); } + int get_int() const { return static_cast(num_val_); } + const std::string& get_string() const { return str_val_; } + + // Implicit conversions + operator bool() const { return type_ == type_t::boolean ? bool_val_ : false; } + operator int() const { return static_cast(num_val_); } + operator int64_t() const { return static_cast(num_val_); } + operator uint64_t() const { return static_cast(num_val_); } + operator double() const { return num_val_; } + operator std::string() const { return str_val_; } + + // Push back for arrays + void push_back(const json& val) { + if (type_ != type_t::array) { + type_ = type_t::array; + arr_val_.clear(); + } + arr_val_.push_back(val); + } + + // Serialization + std::string dump(int indent = -1) const { + std::ostringstream ss; + dump_impl(ss, indent >= 0 ? indent : -1, 0); + return ss.str(); + } + + static json parse(const std::string& str) { + size_t pos = 0; + return parse_impl(str, pos); + } + + static json array() { + json j; + j.type_ = type_t::array; + return j; + } + + static json object() { + json j; + j.type_ = type_t::object; + return j; + } + + // Iterator support for arrays + std::vector::iterator begin() { return arr_val_.begin(); } + std::vector::iterator end() { return arr_val_.end(); } + std::vector::const_iterator begin() const { return arr_val_.begin(); } + std::vector::const_iterator end() const { return arr_val_.end(); } + + // Get object items + std::map::const_iterator obj_begin() const { return obj_val_.begin(); } + std::map::const_iterator obj_end() const { return obj_val_.end(); } + +private: + type_t type_ = type_t::null; + bool bool_val_ = false; + double num_val_ = 0.0; + std::string str_val_; + std::vector arr_val_; + std::map obj_val_; + + void dump_impl(std::ostringstream& ss, int indent, int level) const { + switch (type_) { + case type_t::null: + ss << "null"; + break; + case type_t::boolean: + ss << (bool_val_ ? "true" : "false"); + break; + case type_t::number: + if (num_val_ == static_cast(num_val_)) { + ss << static_cast(num_val_); + } else { + ss << num_val_; + } + break; + case type_t::string: + ss << '"'; + for (char c : str_val_) { + switch (c) { + case '"': ss << "\\\""; break; + case '\\': ss << "\\\\"; break; + case '\n': ss << "\\n"; break; + case '\r': ss << "\\r"; break; + case '\t': ss << "\\t"; break; + default: ss << c; + } + } + ss << '"'; + break; + case type_t::array: + ss << '['; + if (indent >= 0) { + ss << '\n'; + for (int i = 0; i <= level; ++i) ss << std::string(indent, ' '); + } + for (size_t i = 0; i < arr_val_.size(); ++i) { + if (i > 0) { + ss << ','; + if (indent >= 0) ss << '\n' << std::string((level + 1) * indent, ' '); + } + arr_val_[i].dump_impl(ss, indent, level + 1); + } + if (indent >= 0) ss << '\n' << std::string(level * indent, ' '); + ss << ']'; + break; + case type_t::object: + ss << '{'; + if (indent >= 0) ss << '\n' << std::string((level + 1) * indent, ' '); + bool first = true; + for (const auto& p : obj_val_) { + if (!first) { + ss << ','; + if (indent >= 0) ss << '\n' << std::string((level + 1) * indent, ' '); + } + first = false; + ss << '"' << p.first << "\":"; + if (indent >= 0) ss << ' '; + p.second.dump_impl(ss, indent, level + 1); + } + if (indent >= 0) ss << '\n' << std::string(level * indent, ' '); + ss << '}'; + break; + } + } + + static json parse_impl(const std::string& str, size_t& pos) { + skip_whitespace(str, pos); + if (pos >= str.size()) throw std::runtime_error("Unexpected end of input"); + + char c = str[pos]; + if (c == 'n') { + pos += 4; + return json(); + } else if (c == 't') { + pos += 4; + return json(true); + } else if (c == 'f') { + pos += 5; + return json(false); + } else if (c == '"') { + return json(parse_string(str, pos)); + } else if (c == '[') { + return parse_array(str, pos); + } else if (c == '{') { + return parse_object(str, pos); + } else if (c == '-' || std::isdigit(c)) { + return parse_number(str, pos); + } + throw std::runtime_error(std::string("Unexpected character: ") + c); + } + + static void skip_whitespace(const std::string& str, size_t& pos) { + while (pos < str.size() && std::isspace(str[pos])) ++pos; + } + + static std::string parse_string(const std::string& str, size_t& pos) { + ++pos; // skip opening quote + std::string result; + while (pos < str.size() && str[pos] != '"') { + if (str[pos] == '\\' && pos + 1 < str.size()) { + ++pos; + switch (str[pos]) { + case '"': result += '"'; break; + case '\\': result += '\\'; break; + case 'n': result += '\n'; break; + case 'r': result += '\r'; break; + case 't': result += '\t'; break; + default: result += str[pos]; + } + } else { + result += str[pos]; + } + ++pos; + } + if (pos < str.size()) ++pos; // skip closing quote + return result; + } + + static json parse_number(const std::string& str, size_t& pos) { + size_t start = pos; + if (str[pos] == '-') ++pos; + while (pos < str.size() && std::isdigit(str[pos])) ++pos; + if (pos < str.size() && str[pos] == '.') { + ++pos; + while (pos < str.size() && std::isdigit(str[pos])) ++pos; + } + if (pos < str.size() && (str[pos] == 'e' || str[pos] == 'E')) { + ++pos; + if (pos < str.size() && (str[pos] == '+' || str[pos] == '-')) ++pos; + while (pos < str.size() && std::isdigit(str[pos])) ++pos; + } + return json(std::stod(str.substr(start, pos - start))); + } + + static json parse_array(const std::string& str, size_t& pos) { + json arr = json::array(); + ++pos; // skip '[' + skip_whitespace(str, pos); + if (pos < str.size() && str[pos] == ']') { + ++pos; + return arr; + } + while (pos < str.size()) { + arr.push_back(parse_impl(str, pos)); + skip_whitespace(str, pos); + if (pos >= str.size() || str[pos] == ']') { + ++pos; + break; + } + if (str[pos] == ',') ++pos; + skip_whitespace(str, pos); + } + return arr; + } + + static json parse_object(const std::string& str, size_t& pos) { + json obj = json::object(); + ++pos; // skip '{' + skip_whitespace(str, pos); + if (pos < str.size() && str[pos] == '}') { + ++pos; + return obj; + } + while (pos < str.size()) { + skip_whitespace(str, pos); + if (str[pos] != '"') throw std::runtime_error("Expected string key"); + std::string key = parse_string(str, pos); + skip_whitespace(str, pos); + if (pos >= str.size() || str[pos] != ':') throw std::runtime_error("Expected ':'"); + ++pos; + obj[key] = parse_impl(str, pos); + skip_whitespace(str, pos); + if (pos >= str.size() || str[pos] == '}') { + ++pos; + break; + } + if (str[pos] == ',') ++pos; + } + return obj; + } +}; + +// Type alias for compatibility +using nlohmann_json = json; + +} // namespace simple + +namespace nlohmann { + using json = simple::json; +} + +#endif // SIMPLE_JSON_HPP diff --git a/sdk/cpp/simple_test.cpp b/sdk/cpp/simple_test.cpp new file mode 100644 index 0000000..aafd5d7 --- /dev/null +++ b/sdk/cpp/simple_test.cpp @@ -0,0 +1,447 @@ +// 独立测试程序 - 不依赖外部库 +// 使用Windows原生WinHTTP进行HTTP请求 + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "winhttp.lib") +#else +#include +#include +#include +#include +#endif + +#include "json.hpp" +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// ============================================================================ +// WinHTTP客户端 +// ============================================================================ +class HttpClient { +public: + HttpClient() { +#ifdef _WIN32 + hSession_ = WinHttpOpen(L"VerifySDK/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); +#endif + } + + ~HttpClient() { +#ifdef _WIN32 + if (hConnect_) WinHttpCloseHandle(hConnect_); + if (hSession_) WinHttpCloseHandle(hSession_); +#endif + } + + std::string get(const std::string& url) { + return request(url, "GET", ""); + } + + std::string post(const std::string& url, const std::string& body) { + return request(url, "POST", body); + } + +private: +#ifdef _WIN32 + HINTERNET hSession_ = nullptr; + HINTERNET hConnect_ = nullptr; +#endif + + std::string request(const std::string& url, const std::string& method, const std::string& body) { + std::string result; +#ifdef _WIN32 + // 解析URL + std::wstring wurl(url.begin(), url.end()); + URL_COMPONENTS uc = { sizeof(uc) }; + wchar_t host[256] = {0}; + wchar_t path[1024] = {0}; + uc.lpszHostName = host; + uc.dwHostNameLength = 256; + uc.lpszUrlPath = path; + uc.dwUrlPathLength = 1024; + + if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &uc)) { + throw std::runtime_error("Failed to parse URL"); + } + + // 连接服务器 + std::wstring whost(host); + hConnect_ = WinHttpConnect(hSession_, whost.c_str(), uc.nPort, 0); + if (!hConnect_) { + throw std::runtime_error("Failed to connect to server"); + } + + // 创建请求 + std::wstring wpath(path); + std::wstring wmethod(method.begin(), method.end()); + DWORD flags = (uc.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0; + HINTERNET hRequest = WinHttpOpenRequest(hConnect_, wmethod.c_str(), wpath.c_str(), + nullptr, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, flags); + if (!hRequest) { + throw std::runtime_error("Failed to create request"); + } + + // 发送请求 + LPCWSTR headers = L"Content-Type: application/json\r\n"; + BOOL bResult = WinHttpSendRequest(hRequest, headers, -1, + (LPVOID)body.c_str(), body.length(), + body.length(), 0); + if (!bResult) { + WinHttpCloseHandle(hRequest); + throw std::runtime_error("Failed to send request"); + } + + // 接收响应 + bResult = WinHttpReceiveResponse(hRequest, nullptr); + if (!bResult) { + WinHttpCloseHandle(hRequest); + throw std::runtime_error("Failed to receive response"); + } + + // 读取数据 + DWORD dwSize = 0; + DWORD dwDownloaded = 0; + do { + dwSize = 0; + if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) break; + if (dwSize == 0) break; + + std::vector buffer(dwSize + 1); + if (WinHttpReadData(hRequest, &buffer[0], dwSize, &dwDownloaded)) { + result.append(buffer.data(), dwDownloaded); + } + } while (dwSize > 0); + + WinHttpCloseHandle(hRequest); +#else + // Linux实现(简化版) + result = curl_request(url, method, body); +#endif + return result; + } + +#ifndef _WIN32 + std::string curl_request(const std::string& url, const std::string& method, const std::string& body) { + // 简化的socket实现(仅用于演示) + return "{\"code\": 200, \"message\": \"success\", \"data\": {}}"; + } +#endif +}; + +// ============================================================================ +// 验证客户端 +// ============================================================================ +class VerifyClient { +public: + VerifyClient(const std::string& baseUrl, const std::string& appKey) + : baseUrl_(baseUrl), appKey_(appKey) { + // 移除末尾斜杠 + if (!baseUrl_.empty() && baseUrl_.back() == '/') { + baseUrl_.pop_back(); + } + } + + void setToken(const std::string& token) { + token_ = token; + } + + // 获取应用信息 + json getAppInfo() { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/info"; + std::string response = http_.get(url); + return json::parse(response); + } + + // 检查更新 + json checkUpdate(const std::string& version = "") { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/check-update"; + if (!version.empty()) { + url += "?version=" + version; + } + std::string response = http_.get(url); + return json::parse(response); + } + + // 用户登录 + json login(const std::string& username, const std::string& password, + const std::string& deviceId) { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/login"; + + json body; + body["username"] = username; + body["password"] = password; + body["device_id"] = deviceId; + + std::string response = http_.post(url, body.dump()); + json result = json::parse(response); + + // 自动设置token + if (result["code"].get_int() == 200 && result["data"].contains("token")) { + token_ = result["data"]["token"].get_string(); + } + + return result; + } + + // 用户注册 + json registerUser(const std::string& username, const std::string& password, + const std::string& deviceId) { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/register"; + + json body; + body["username"] = username; + body["password"] = password; + body["device_id"] = deviceId; + + std::string response = http_.post(url, body.dump()); + return json::parse(response); + } + + // 心跳 + json heartbeat(uint64_t userId, const std::string& deviceId, + const std::string& instanceId = "") { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/heartbeat"; + + json body; + body["user_id"] = userId; + body["device_id"] = deviceId; + if (!instanceId.empty()) { + body["instance_id"] = instanceId; + } + + std::string response = http_.post(url, body.dump()); + return json::parse(response); + } + + // 卡密充值 + json recharge(const std::string& username, const std::string& cardKey, + const std::string& deviceId) { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/recharge"; + + json body; + body["username"] = username; + body["card_key"] = cardKey; + body["device_id"] = deviceId; + + std::string response = http_.post(url, body.dump()); + return json::parse(response); + } + + // 获取设备列表 + json getDevices() { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/devices"; + std::string response = http_.get(url); + return json::parse(response); + } + + // 获取云端变量 + json getVariables() { + std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/variables"; + std::string response = http_.get(url); + return json::parse(response); + } + +private: + std::string baseUrl_; + std::string appKey_; + std::string token_; + HttpClient http_; +}; + +// ============================================================================ +// 测试函数 +// ============================================================================ +void printSeparator(const std::string& title) { + std::cout << "\n"; + std::cout << "========================================\n"; + std::cout << title << "\n"; + std::cout << "========================================\n"; +} + +void testJson() { + printSeparator("JSON功能测试"); + + // 创建JSON对象 + json obj = json::object(); + obj["name"] = "测试应用"; + obj["version"] = "1.0.0"; + obj["active"] = true; + obj["count"] = 100; + + std::cout << "[创建对象] "; + std::cout << obj.dump() << "\n"; + + // 创建数组 + json arr = json::array(); + arr.push_back("item1"); + arr.push_back("item2"); + arr.push_back(123); + + std::cout << "[创建数组] "; + std::cout << arr.dump() << "\n"; + + // 解析JSON + std::string jsonStr = R"({"code":200,"message":"success","data":{"user_id":1,"token":"abc123"}})"; + json parsed = json::parse(jsonStr); + + std::cout << "[解析结果]\n"; + std::cout << " code: " << parsed["code"].get_int() << "\n"; + std::cout << " message: " << parsed["message"].get_string() << "\n"; + std::cout << " user_id: " << parsed["data"]["user_id"].get_uint64() << "\n"; + + std::cout << "\nJSON测试通过!\n"; +} + +void testApi(const std::string& baseUrl, const std::string& appKey) { + printSeparator("API测试"); + + std::cout << "服务器: " << baseUrl << "\n"; + std::cout << "AppKey: " << appKey << "\n\n"; + + VerifyClient client(baseUrl, appKey); + + // 测试获取应用信息 + std::cout << "[测试] 获取应用信息...\n"; + try { + json info = client.getAppInfo(); + if (info["code"].get_int() == 200) { + std::cout << " [成功] 应用名称: " << info["data"]["name"].get_string() << "\n"; + std::cout << " [成功] 状态: " << info["data"]["status"].get_string() << "\n"; + } else { + std::cout << " [错误] " << info["message"].get_string() << "\n"; + } + } catch (const std::exception& e) { + std::cout << " [异常] " << e.what() << "\n"; + } + + // 测试检查更新 + std::cout << "\n[测试] 检查更新...\n"; + try { + json update = client.checkUpdate("1.0.0"); + if (update["code"].get_int() == 200) { + std::cout << " [成功] 最新版本: " << update["data"]["latest_version"].get_string() << "\n"; + std::cout << " [成功] 有更新: " << (update["data"]["has_update"].get_bool() ? "是" : "否") << "\n"; + } else { + std::cout << " [错误] " << update["message"].get_string() << "\n"; + } + } catch (const std::exception& e) { + std::cout << " [异常] " << e.what() << "\n"; + } + + // 测试不存在的版本 + std::cout << "\n[测试] 检查不存在的版本...\n"; + try { + json update = client.checkUpdate("99.99.99"); + if (update["code"].get_int() == 200) { + std::cout << " [意外成功]\n"; + } else { + std::cout << " [预期错误] " << update["message"].get_string() << "\n"; + } + } catch (const std::exception& e) { + std::cout << " [预期异常] " << e.what() << "\n"; + } + + // 测试登录 + std::cout << "\n[测试] 用户登录...\n"; + try { + // 使用测试用户 + json loginResult = client.login("test_cpp_user", "test123456", "cpp_test_device"); + if (loginResult["code"].get_int() == 200) { + std::cout << " [成功] 用户ID: " << loginResult["data"]["user_id"].get_uint64() << "\n"; + std::cout << " [成功] Token已获取\n"; + + uint64_t userId = loginResult["data"]["user_id"].get_uint64(); + + // 测试心跳 + std::cout << "\n[测试] 心跳上报...\n"; + json heartbeat = client.heartbeat(userId, "cpp_test_device", "instance_001"); + if (heartbeat["code"].get_int() == 200) { + std::cout << " [成功] 心跳成功\n"; + if (heartbeat["data"].contains("balance")) { + std::cout << " [信息] 余额: " << heartbeat["data"]["balance"].get_double() << "\n"; + } + } + } else { + std::cout << " [错误] " << loginResult["message"].get_string() << "\n"; + } + } catch (const std::exception& e) { + std::cout << " [异常] " << e.what() << "\n"; + } + + // 测试卡密充值(预期失败) + std::cout << "\n[测试] 卡密充值(无效卡密)...\n"; + try { + json recharge = client.recharge("admin", "invalid_card_key", "cpp_test_device"); + if (recharge["code"].get_int() == 200) { + std::cout << " [意外成功] 充值成功\n"; + } else { + std::cout << " [预期错误] " << recharge["message"].get_string() << "\n"; + } + } catch (const std::exception& e) { + std::cout << " [预期异常] " << e.what() << "\n"; + } +} + +// ============================================================================ +// 主函数 +// ============================================================================ +int main(int argc, char* argv[]) { + std::cout << R"( +╔════════════════════════════════════════════════════════════╗ +║ 验证平台 SDK C++ 独立测试 ║ +║ Verify Platform SDK C++ Test ║ +╚════════════════════════════════════════════════════════════╝ +)" << "\n"; + + // 配置 + std::string baseUrl = "https://gendan.xyz"; + std::string appKey = "your_app_key"; // 替换为实际的appKey + + // 解析命令行参数 + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--help" || arg == "-h") { + std::cout << "用法: " << argv[0] << " [选项]\n"; + std::cout << "选项:\n"; + std::cout << " --help, -h 显示帮助信息\n"; + std::cout << " --url 设置服务器URL\n"; + std::cout << " --key 设置应用密钥\n"; + std::cout << " --json-only 仅测试JSON功能\n"; + return 0; + } else if (arg == "--url" && i + 1 < argc) { + baseUrl = argv[++i]; + } else if (arg == "--key" && i + 1 < argc) { + appKey = argv[++i]; + } else if (arg == "--json-only") { + testJson(); + return 0; + } + } + + try { + // JSON测试 + testJson(); + + // API测试 + testApi(baseUrl, appKey); + + std::cout << "\n========================================\n"; + std::cout << "所有测试完成!\n"; + std::cout << "========================================\n"; + + } catch (const std::exception& e) { + std::cerr << "\n错误: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/sdk/cpp/test_main.cpp b/sdk/cpp/test_main.cpp new file mode 100644 index 0000000..c7442b5 --- /dev/null +++ b/sdk/cpp/test_main.cpp @@ -0,0 +1,361 @@ +#include "verify_client.hpp" +#include +#include +#include +#include + +using namespace verify; + +// 测试配置 +const std::string BASE_URL = "https://gendan.xyz"; +const std::string APP_KEY = "your_app_key"; // 需要替换为实际的appKey +const std::string TEST_USERNAME = "test_user_cpp"; +const std::string TEST_PASSWORD = "test123456"; +const std::string TEST_DEVICE_ID = "cpp_test_device_001"; + +class TestRunner { +public: + void run() { + std::cout << "========================================\n"; + std::cout << "验证平台SDK C++ 测试\n"; + std::cout << "========================================\n\n"; + + // 初始化客户端 + testClientCreation(); + + // 测试应用信息接口 + testAppInfo(); + + // 测试用户注册和登录 + testUserRegistration(); + testUserLogin(); + + // 测试账户接口 + if (!token_.empty()) { + testAccount(); + testHeartbeat(); + } + + // 测试充值接口 + testRecharge(); + + // 测试设备管理 + if (!token_.empty()) { + testDeviceManagement(); + } + + // 测试云端数据 + if (!token_.empty()) { + testCloudData(); + } + + std::cout << "\n========================================\n"; + std::cout << "所有测试完成!\n"; + std::cout << "========================================\n"; + } + +private: + std::unique_ptr client_; + std::string token_; + uint64_t userId_ = 0; + + void testClientCreation() { + std::cout << "[测试] 客户端创建... "; + try { + client_ = std::make_unique(BASE_URL, APP_KEY); + client_->setTimeout(30); + std::cout << "通过\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + throw; + } + } + + void testAppInfo() { + std::cout << "[测试] 获取应用信息... "; + try { + AppConfig config = client_->getAppInfo(); + std::cout << "通过\n"; + std::cout << " - 应用名称: " << config.name << "\n"; + std::cout << " - 计费类型: " << config.billingType << "\n"; + std::cout << " - 最大设备数: " << config.maxDevices << "\n"; + std::cout << " - 心跳间隔: " << config.heartbeatInterval << "秒\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + + std::cout << "[测试] 检查更新... "; + try { + VersionInfo version = client_->checkUpdate("1.0.0"); + std::cout << "通过\n"; + std::cout << " - 有更新: " << (version.hasUpdate ? "是" : "否") << "\n"; + std::cout << " - 最新版本: " << version.latestVersion << "\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + + std::cout << "[测试] 获取公告列表... "; + try { + auto announcements = client_->getAnnouncements(); + std::cout << "通过 (共" << announcements.size() << "条公告)\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } + + void testUserRegistration() { + std::cout << "[测试] 用户注册... "; + try { + // 先尝试登录,如果用户存在则跳过注册 + try { + auto result = client_->login(TEST_USERNAME, TEST_PASSWORD, TEST_DEVICE_ID); + userId_ = result["user_id"].get(); + token_ = result["token"].get(); + std::cout << "用户已存在,跳过注册\n"; + return; + } catch (...) { + // 用户不存在,继续注册 + } + + userId_ = client_->registerUser( + TEST_USERNAME, + TEST_PASSWORD, + TEST_DEVICE_ID, + "C++测试设备", + "windows", + "instance_001" + ); + std::cout << "通过 (用户ID: " << userId_ << ")\n"; + + // 注册后登录 + auto result = client_->login(TEST_USERNAME, TEST_PASSWORD, TEST_DEVICE_ID); + token_ = result["token"].get(); + } catch (const ApiException& e) { + std::cout << "API错误 [" << e.code() << "]: " << e.what() << "\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } + + void testUserLogin() { + std::cout << "[测试] 用户登录... "; + try { + auto result = client_->login(TEST_USERNAME, TEST_PASSWORD, TEST_DEVICE_ID); + userId_ = result["user_id"].get(); + token_ = result["token"].get(); + std::cout << "通过\n"; + std::cout << " - 用户ID: " << userId_ << "\n"; + std::cout << " - Token: " << token_.substr(0, 30) << "...\n"; + } catch (const ApiException& e) { + std::cout << "API错误 [" << e.code() << "]: " << e.what() << "\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } + + void testAccount() { + std::cout << "[测试] 获取账户信息... "; + try { + UserInfo info = client_->getAccount(userId_); + std::cout << "通过\n"; + std::cout << " - 用户名: " << info.username << "\n"; + std::cout << " - 余额: " << info.balance << "\n"; + std::cout << " - 状态: " << info.status << "\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } + + void testHeartbeat() { + std::cout << "[测试] 心跳上报... "; + try { + auto result = client_->heartbeat(userId_, TEST_DEVICE_ID, "instance_001"); + std::cout << "通过\n"; + if (result.contains("balance")) { + std::cout << " - 当前余额: " << result["balance"] << "\n"; + } + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } + + void testRecharge() { + std::cout << "[测试] 卡密充值 (使用无效卡密测试错误处理)... "; + try { + auto result = client_->recharge(TEST_USERNAME, "invalid_card_key_12345", TEST_DEVICE_ID); + std::cout << "未预期的成功\n"; + } catch (const ApiException& e) { + std::cout << "通过 (预期错误: " << e.what() << ")\n"; + } catch (const std::exception& e) { + std::cout << "通过 (预期错误: " << e.what() << ")\n"; + } + } + + void testDeviceManagement() { + std::cout << "[测试] 获取设备列表... "; + try { + auto devices = client_->getDevices(); + std::cout << "通过 (共" << devices.size() << "台设备)\n"; + for (const auto& d : devices) { + std::cout << " - 设备ID: " << d.deviceId << " (" << d.deviceName << ")\n"; + } + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + + std::cout << "[测试] 获取设备数量... "; + try { + auto result = client_->getDeviceCount(userId_); + std::cout << "通过\n"; + std::cout << " - 已绑定: " << result["count"] << "\n"; + std::cout << " - 最大数: " << result["max_devices"] << "\n"; + std::cout << " - 剩余: " << result["remaining"] << "\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + + std::cout << "[测试] 获取实例列表... "; + try { + auto instances = client_->getInstances(userId_); + std::cout << "通过 (共" << instances.size() << "个实例)\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } + + void testCloudData() { + std::cout << "[测试] 获取云端常量... "; + try { + auto constants = client_->getConstants(); + std::cout << "通过 (共" << constants.size() << "个常量)\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + + std::cout << "[测试] 获取云端变量... "; + try { + auto variables = client_->getVariables(); + std::cout << "通过 (共" << variables.size() << "个变量)\n"; + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } +}; + +// 加密测试 +void testCrypto() { + std::cout << "\n[测试] 加密功能...\n"; + + // AES测试 + { + std::cout << " [AES加密/解密] "; + try { + Crypto aes(EncryptType::AES, "test_key_12345678"); + std::string plaintext = "Hello, World! 你好,世界!"; + std::string encrypted = aes.encrypt(plaintext); + std::string decrypted = aes.decrypt(encrypted); + + if (decrypted == plaintext) { + std::cout << "通过\n"; + std::cout << " 原文: " << plaintext << "\n"; + std::cout << " 密文: " << encrypted.substr(0, 50) << "...\n"; + } else { + std::cout << "失败: 解密结果不匹配\n"; + } + } catch (const CryptoException& e) { + std::cout << "失败: " << e.what() << "\n"; + std::cout << " 提示: 请使用 -DUSE_OPENSSL 编译以启用AES加密\n"; + } + } + + // RC4测试 + { + std::cout << " [RC4加密/解密] "; + try { + Crypto rc4(EncryptType::RC4, "rc4_secret_key"); + std::string plaintext = "Test RC4 encryption"; + std::string encrypted = rc4.encrypt(plaintext); + std::string decrypted = rc4.decrypt(encrypted); + + if (decrypted == plaintext) { + std::cout << "通过\n"; + } else { + std::cout << "失败: 解密结果不匹配\n"; + } + } catch (const std::exception& e) { + std::cout << "失败: " << e.what() << "\n"; + } + } +} + +// 性能测试 +void testPerformance() { + std::cout << "\n[测试] 性能测试...\n"; + + Client client(BASE_URL, APP_KEY); + client.setTimeout(10); + + int successCount = 0; + int failCount = 0; + const int iterations = 10; + + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < iterations; ++i) { + try { + client.getAppInfo(); + ++successCount; + } catch (...) { + ++failCount; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + std::cout << " - 请求次数: " << iterations << "\n"; + std::cout << " - 成功次数: " << successCount << "\n"; + std::cout << " - 失败次数: " << failCount << "\n"; + std::cout << " - 总耗时: " << duration.count() << "ms\n"; + std::cout << " - 平均耗时: " << (duration.count() / iterations) << "ms/请求\n"; +} + +int main(int argc, char* argv[]) { + std::cout << R"( +╔════════════════════════════════════════════════════════════╗ +║ 验证平台 SDK C++ 测试程序 ║ +║ Verify Platform SDK C++ Test ║ +╚════════════════════════════════════════════════════════════╝ +)" << "\n"; + + // 解析命令行参数 + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--help" || arg == "-h") { + std::cout << "用法: " << argv[0] << " [选项]\n"; + std::cout << "选项:\n"; + std::cout << " --help, -h 显示帮助信息\n"; + std::cout << " --crypto 仅测试加密功能\n"; + std::cout << " --perf 仅测试性能\n"; + return 0; + } + } + + try { + // 测试加密 + testCrypto(); + + // 主测试 + TestRunner runner; + runner.run(); + + // 性能测试 + testPerformance(); + + } catch (const std::exception& e) { + std::cerr << "\n错误: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/sdk/cpp/vcpkg.json b/sdk/cpp/vcpkg.json new file mode 100644 index 0000000..c0e7c46 --- /dev/null +++ b/sdk/cpp/vcpkg.json @@ -0,0 +1,10 @@ +{ + "name": "verify-sdk", + "version": "1.0.0", + "description": "C++ SDK for Verify Platform", + "dependencies": [ + "curl", + "nlohmann-json", + "openssl" + ] +} \ No newline at end of file diff --git a/sdk/cpp/verify_client.cpp b/sdk/cpp/verify_client.cpp new file mode 100644 index 0000000..5cf05fc --- /dev/null +++ b/sdk/cpp/verify_client.cpp @@ -0,0 +1,830 @@ +#include "verify_client.hpp" +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_OPENSSL +#include +#include +#include +#include +#endif + +namespace verify { + +// ============================================================================ +// Crypto Implementation +// ============================================================================ + +Crypto::Crypto(EncryptType type, const std::string& key) + : type_(type), key_(key) { +} + +Crypto::~Crypto() = default; + +std::vector Crypto::padKey(const std::vector& key, size_t targetLen) { + if (key.size() >= targetLen) { + return std::vector(key.begin(), key.begin() + targetLen); + } + + std::vector padded(targetLen); + std::copy(key.begin(), key.end(), padded.begin()); + for (size_t i = key.size(); i < targetLen; ++i) { + padded[i] = key[i % key.size()]; + } + return padded; +} + +std::string base64Encode(const std::vector& data) { +#ifdef USE_OPENSSL + BIO* bio = BIO_new(BIO_s_mem()); + BIO* b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_push(b64, bio); + + BIO_write(b64, data.data(), static_cast(data.size())); + BIO_flush(b64); + + BUF_MEM* buffer; + BIO_get_mem_ptr(b64, &buffer); + + std::string result(buffer->data, buffer->length); + BIO_free_all(b64); + + return result; +#else + static const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string result; + result.reserve(((data.size() + 2) / 3) * 4); + + for (size_t i = 0; i < data.size(); i += 3) { + uint32_t n = (data[i] << 16); + if (i + 1 < data.size()) n |= (data[i + 1] << 8); + if (i + 2 < data.size()) n |= data[i + 2]; + + result.push_back(chars[(n >> 18) & 0x3F]); + result.push_back(chars[(n >> 12) & 0x3F]); + result.push_back((i + 1 < data.size()) ? chars[(n >> 6) & 0x3F] : '='); + result.push_back((i + 2 < data.size()) ? chars[n & 0x3F] : '='); + } + return result; +#endif +} + +std::vector base64Decode(const std::string& encoded) { +#ifdef USE_OPENSSL + BIO* bio = BIO_new_mem_buf(encoded.data(), static_cast(encoded.size())); + BIO* b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_push(b64, bio); + + std::vector result(encoded.size()); + int len = BIO_read(b64, result.data(), static_cast(result.size())); + result.resize(len > 0 ? len : 0); + + BIO_free_all(b64); + return result; +#else + static const int table[] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, + 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, + 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, + -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, + 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1 + }; + + std::vector result; + result.reserve(encoded.size() * 3 / 4); + + int val = 0, bits = 0; + for (char c : encoded) { + if (c == '=') break; + int v = table[static_cast(c)]; + if (v < 0) continue; + + val = (val << 6) | v; + bits += 6; + + if (bits >= 8) { + bits -= 8; + result.push_back(static_cast((val >> bits) & 0xFF)); + } + } + return result; +#endif +} + +std::string Crypto::encrypt(const std::string& plaintext) { + if (type_ == EncryptType::None || key_.empty()) { + return plaintext; + } + + switch (type_) { + case EncryptType::AES: + return encryptAES(plaintext); + case EncryptType::RC4: + return encryptRC4(plaintext); + default: + return plaintext; + } +} + +std::string Crypto::decrypt(const std::string& ciphertext) { + if (type_ == EncryptType::None || key_.empty()) { + return ciphertext; + } + + switch (type_) { + case EncryptType::AES: + return decryptAES(ciphertext); + case EncryptType::RC4: + return decryptRC4(ciphertext); + default: + return ciphertext; + } +} + +std::string Crypto::encryptAES(const std::string& plaintext) { +#ifdef USE_OPENSSL + std::vector keyBytes = padKey(std::vector(key_.begin(), key_.end()), 32); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) throw CryptoException("Failed to create cipher context"); + + std::vector nonce(12); + RAND_bytes(nonce.data(), 12); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw CryptoException("Failed to init encryption"); + } + + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr); + EVP_EncryptInit_ex(ctx, nullptr, nullptr, keyBytes.data(), nonce.data()); + + std::vector ciphertext(plaintext.size() + 16); + int len; + EVP_EncryptUpdate(ctx, ciphertext.data(), &len, + reinterpret_cast(plaintext.data()), plaintext.size()); + + int ciphertextLen = len; + EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len); + ciphertextLen += len; + + std::vector tag(16); + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag.data()); + + EVP_CIPHER_CTX_free(ctx); + + // nonce (12) + ciphertext + tag (16) + std::vector result; + result.reserve(12 + ciphertextLen + 16); + result.insert(result.end(), nonce.begin(), nonce.end()); + result.insert(result.end(), ciphertext.begin(), ciphertext.begin() + ciphertextLen); + result.insert(result.end(), tag.begin(), tag.end()); + + return base64Encode(result); +#else + throw CryptoException("OpenSSL not available. Compile with -DUSE_OPENSSL"); +#endif +} + +std::string Crypto::decryptAES(const std::string& ciphertext) { +#ifdef USE_OPENSSL + std::vector data = base64Decode(ciphertext); + if (data.size() < 28) throw CryptoException("Ciphertext too short"); + + std::vector keyBytes = padKey(std::vector(key_.begin(), key_.end()), 32); + + std::vector nonce(data.begin(), data.begin() + 12); + std::vector tag(data.end() - 16, data.end()); + std::vector encrypted(data.begin() + 12, data.end() - 16); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) throw CryptoException("Failed to create cipher context"); + + EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr); + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr); + EVP_DecryptInit_ex(ctx, nullptr, nullptr, keyBytes.data(), nonce.data()); + + std::vector decrypted(encrypted.size()); + int len; + EVP_DecryptUpdate(ctx, decrypted.data(), &len, encrypted.data(), encrypted.size()); + + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag.data()); + + int ret = EVP_DecryptFinal_ex(ctx, decrypted.data() + len, &len); + EVP_CIPHER_CTX_free(ctx); + + if (ret != 1) throw CryptoException("Decryption failed: authentication error"); + + return std::string(decrypted.begin(), decrypted.begin() + len); +#else + throw CryptoException("OpenSSL not available. Compile with -DUSE_OPENSSL"); +#endif +} + +class RC4 { +public: + explicit RC4(const std::vector& key) { + for (int i = 0; i < 256; ++i) s_[i] = static_cast(i); + + uint8_t j = 0; + for (int i = 0; i < 256; ++i) { + j = j + s_[i] + key[i % key.size()]; + std::swap(s_[i], s_[j]); + } + } + + void process(std::vector& data) { + for (auto& byte : data) { + i_++; + j_ += s_[i_]; + std::swap(s_[i_], s_[j_]); + byte ^= s_[(s_[i_] + s_[j_]) & 0xFF]; + } + } + +private: + uint8_t s_[256] = {}; + uint8_t i_ = 0, j_ = 0; +}; + +std::string Crypto::encryptRC4(const std::string& plaintext) { + std::vector keyBytes(key_.begin(), key_.end()); + if (keyBytes.empty()) throw CryptoException("RC4 key cannot be empty"); + + std::vector data(plaintext.begin(), plaintext.end()); + RC4 rc4(keyBytes); + rc4.process(data); + + return base64Encode(data); +} + +std::string Crypto::decryptRC4(const std::string& ciphertext) { + std::vector keyBytes(key_.begin(), key_.end()); + if (keyBytes.empty()) throw CryptoException("RC4 key cannot be empty"); + + std::vector data = base64Decode(ciphertext); + RC4 rc4(keyBytes); + rc4.process(data); + + return std::string(data.begin(), data.end()); +} + +// ============================================================================ +// HttpClient Implementation +// ============================================================================ + +HttpClient::HttpClient() { + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +HttpClient::~HttpClient() { + curl_global_cleanup(); +} + +void HttpClient::setBaseUrl(const std::string& url) { + baseUrl_ = url; + if (!baseUrl_.empty() && baseUrl_.back() == '/') { + baseUrl_.pop_back(); + } +} + +void HttpClient::setTimeout(int seconds) { + timeout_ = seconds; +} + +void HttpClient::addHeader(const std::string& key, const std::string& value) { + headers_[key] = value; +} + +void HttpClient::clearHeaders() { + headers_.clear(); +} + +void HttpClient::setCrypto(std::shared_ptr crypto) { + crypto_ = crypto; +} + +std::string HttpClient::buildUrl(const std::string& path) { + return baseUrl_ + path; +} + +size_t HttpClient::writeCallback(void* contents, size_t size, size_t nmemb, std::string* userp) { + size_t totalSize = size * nmemb; + userp->append(static_cast(contents), totalSize); + return totalSize; +} + +size_t HttpClient::writeBinaryCallback(void* contents, size_t size, size_t nmemb, std::vector* userp) { + size_t totalSize = size * nmemb; + userp->insert(userp->end(), static_cast(contents), static_cast(contents) + totalSize); + return totalSize; +} + +json HttpClient::processResponse(const std::string& response) { + std::string data = response; + + if (crypto_) { + try { + data = crypto_->decrypt(response); + auto j = json::parse(data); + if (j.contains("data") && j["data"].is_string()) { + j["data"] = json::parse(crypto_->decrypt(j["data"].get())); + } + return j; + } catch (...) { + // Try parsing as-is + } + } + + return json::parse(response); +} + +json HttpClient::get(const std::string& path) { + CURL* curl = curl_easy_init(); + if (!curl) throw std::runtime_error("Failed to initialize CURL"); + + std::string response; + struct curl_slist* headers = nullptr; + + for (const auto& h : headers_) { + std::string header = h.first + ": " + h.second; + headers = curl_slist_append(headers, header.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + + CURLcode res = curl_easy_perform(curl); + + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); + } + + auto j = processResponse(response); + + if (j["code"].get() != 200) { + throw ApiException(j["code"].get(), j["message"].get()); + } + + return j; +} + +json HttpClient::post(const std::string& path, const json& body) { + CURL* curl = curl_easy_init(); + if (!curl) throw std::runtime_error("Failed to initialize CURL"); + + std::string response; + struct curl_slist* headers = nullptr; + + headers = curl_slist_append(headers, "Content-Type: application/json"); + for (const auto& h : headers_) { + std::string header = h.first + ": " + h.second; + headers = curl_slist_append(headers, header.c_str()); + } + + std::string bodyStr = body.dump(); + + curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, bodyStr.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + + CURLcode res = curl_easy_perform(curl); + + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); + } + + auto j = processResponse(response); + + if (j["code"].get() != 200) { + throw ApiException(j["code"].get(), j["message"].get()); + } + + return j; +} + +json HttpClient::postForm(const std::string& path, const std::map& fields, const std::map& files) { + CURL* curl = curl_easy_init(); + if (!curl) throw std::runtime_error("Failed to initialize CURL"); + + std::string response; + curl_mime* mime = curl_mime_init(curl); + struct curl_slist* headers = nullptr; + + for (const auto& h : headers_) { + std::string header = h.first + ": " + h.second; + headers = curl_slist_append(headers, header.c_str()); + } + + for (const auto& f : fields) { + curl_mimepart* part = curl_mime_addpart(mime); + curl_mime_name(part, f.first.c_str()); + curl_mime_data(part, f.second.c_str(), CURL_ZERO_TERMINATED); + } + + for (const auto& f : files) { + curl_mimepart* part = curl_mime_addpart(mime); + curl_mime_name(part, f.first.c_str()); + curl_mime_filedata(part, f.second.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + + CURLcode res = curl_easy_perform(curl); + + curl_mime_free(mime); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); + } + + auto j = processResponse(response); + + if (j["code"].get() != 200) { + throw ApiException(j["code"].get(), j["message"].get()); + } + + return j; +} + +std::vector HttpClient::download(const std::string& path) { + CURL* curl = curl_easy_init(); + if (!curl) throw std::runtime_error("Failed to initialize CURL"); + + std::vector response; + struct curl_slist* headers = nullptr; + + for (const auto& h : headers_) { + std::string header = h.first + ": " + h.second; + headers = curl_slist_append(headers, header.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeBinaryCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + + CURLcode res = curl_easy_perform(curl); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); + } + + return response; +} + +// ============================================================================ +// Client Implementation +// ============================================================================ + +Client::Client(const std::string& baseUrl, const std::string& appKey) + : baseUrl_(baseUrl), appKey_(appKey) { + http_ = std::make_shared(); + http_->setBaseUrl(baseUrl_); +} + +Client::~Client() = default; + +void Client::setToken(const std::string& token) { + token_ = token; + http_->addHeader("Authorization", "Bearer " + token_); +} + +void Client::setEncryptType(EncryptType type, const std::string& key) { + encryptType_ = type; + crypto_ = std::make_shared(type, key); + http_->setCrypto(crypto_); +} + +void Client::setTimeout(int seconds) { + http_->setTimeout(seconds); +} + +std::string Client::buildPath(const std::string& endpoint) { + return "/api/v1/app/" + appKey_ + endpoint; +} + +AppConfig Client::getAppInfo() { + auto j = http_->get(buildPath("/info")); + + AppConfig config; + config.id = j["data"]["id"].get(); + config.name = j["data"]["name"].get(); + config.description = j["data"]["description"].get(); + config.iconUrl = j["data"]["icon_url"].get(); + config.status = j["data"]["status"].get(); + config.billingType = j["data"]["billing_type"].get(); + config.loginPolicy = j["data"]["login_policy"].get(); + config.maxDevices = j["data"]["max_devices"].get(); + config.multiOpenMode = j["data"]["multi_open_mode"].get(); + config.maxInstances = j["data"]["max_instances"].get(); + config.enableTrial = j["data"]["enable_trial"].get(); + config.trialBalance = j["data"]["trial_balance"].get(); + config.heartbeatInterval = j["data"]["heartbeat_interval"].get(); + config.heartbeatTimeout = j["data"]["heartbeat_timeout"].get(); + + return config; +} + +VersionInfo Client::checkUpdate(const std::string& version) { + std::string path = buildPath("/check-update"); + if (!version.empty()) { + path += "?version=" + version; + } + + auto j = http_->get(path); + + VersionInfo info; + info.hasUpdate = j["data"]["has_update"].get(); + info.latestVersion = j["data"]["latest_version"].get(); + info.downloadUrl = j["data"]["download_url"].get(); + info.fileSize = j["data"]["file_size"].get(); + info.fileHash = j["data"]["file_hash"].get(); + info.entryFile = j["data"]["entry_file"].get(); + info.updateNotes = j["data"]["update_notes"].get(); + info.updateStrategy = j["data"]["update_strategy"].get(); + info.updateType = j["data"]["update_type"].get(); + info.updateMethod = j["data"]["update_method"].get(); + + return info; +} + +std::vector Client::getAnnouncements() { + auto j = http_->get(buildPath("/announcements")); + return j["data"].get>(); +} + +uint64_t Client::registerUser(const std::string& username, + const std::string& password, + const std::string& deviceId, + const std::string& deviceName, + const std::string& deviceType, + const std::string& instanceId, + const std::string& email, + const std::string& emailCode) { + json body; + body["username"] = username; + body["password"] = password; + body["device_id"] = deviceId; + + if (!deviceName.empty()) body["device_name"] = deviceName; + if (!deviceType.empty()) body["device_type"] = deviceType; + if (!instanceId.empty()) body["instance_id"] = instanceId; + if (!email.empty()) body["email"] = email; + if (!emailCode.empty()) body["email_code"] = emailCode; + + auto j = http_->post(buildPath("/register"), body); + return j["data"]["user_id"].get(); +} + +json Client::login(const std::string& username, + const std::string& password, + const std::string& deviceId, + const std::string& deviceName, + const std::string& deviceType, + const std::string& instanceId) { + json body; + body["username"] = username; + body["password"] = password; + body["device_id"] = deviceId; + + if (!deviceName.empty()) body["device_name"] = deviceName; + if (!deviceType.empty()) body["device_type"] = deviceType; + if (!instanceId.empty()) body["instance_id"] = instanceId; + + auto j = http_->post(buildPath("/login"), body); + + // Auto-set token + if (j["data"].contains("token")) { + setToken(j["data"]["token"].get()); + } + + return j["data"]; +} + +void Client::sendEmailCode(const std::string& email, const std::string& purpose) { + json body; + body["email"] = email; + body["purpose"] = purpose; + + http_->post(buildPath("/send-email-code"), body); +} + +void Client::resetPassword(const std::string& email, const std::string& code, const std::string& newPassword) { + json body; + body["email"] = email; + body["code"] = code; + body["password"] = newPassword; + + http_->post(buildPath("/reset-password"), body); +} + +void Client::changePassword(const std::string& username, const std::string& oldPassword, const std::string& newPassword) { + json body; + body["username"] = username; + body["old_password"] = oldPassword; + body["new_password"] = newPassword; + + http_->post(buildPath("/change-password"), body); +} + +UserInfo Client::getAccount(uint64_t userId) { + json body; + body["user_id"] = userId; + + auto j = http_->post(buildPath("/account"), body); + + UserInfo info; + info.userId = j["data"]["user_id"].get(); + info.username = j["data"]["username"].get(); + info.balance = j["data"]["balance"].get(); + info.status = j["data"]["status"].get(); + + return info; +} + +json Client::heartbeat(uint64_t userId, const std::string& deviceId, const std::string& instanceId) { + json body; + body["user_id"] = userId; + body["device_id"] = deviceId; + if (!instanceId.empty()) body["instance_id"] = instanceId; + + return http_->post(buildPath("/heartbeat"), body)["data"]; +} + +json Client::recharge(const std::string& username, const std::string& cardKey, const std::string& deviceId) { + json body; + body["username"] = username; + body["card_key"] = cardKey; + body["device_id"] = deviceId; + + return http_->post(buildPath("/recharge"), body)["data"]; +} + +json Client::trial(uint64_t userId) { + json body; + body["user_id"] = userId; + + return http_->post(buildPath("/trial"), body)["data"]; +} + +std::vector Client::getDevices() { + auto j = http_->get(buildPath("/devices")); + + std::vector devices; + for (const auto& d : j["data"]) { + DeviceInfo info; + info.id = d["id"].get(); + info.deviceId = d["device_id"].get(); + info.deviceName = d["device_name"].get(); + info.deviceType = d["device_type"].get(); + info.status = d["status"].get(); + info.onlineSessions = d["online_sessions"].get(); + info.createdAt = d["created_at"].get(); + devices.push_back(info); + } + + return devices; +} + +json Client::getDeviceCount(uint64_t userId) { + json body; + body["user_id"] = userId; + + return http_->post(buildPath("/device-count"), body)["data"]; +} + +void Client::unbindDevice(uint64_t userId, const std::string& deviceId) { + json body; + body["user_id"] = userId; + body["device_id"] = deviceId; + + http_->post(buildPath("/unbind-device"), body); +} + +void Client::unbindDeviceWithAuth(const std::string& username, const std::string& password, const std::string& deviceId) { + json body; + body["username"] = username; + body["password"] = password; + body["device_id"] = deviceId; + + http_->post(buildPath("/unbind-device-with-auth"), body); +} + +std::vector Client::getInstances(uint64_t userId) { + json body; + body["user_id"] = userId; + + return http_->post(buildPath("/instances"), body)["data"].get>(); +} + +void Client::forceOfflineInstance(uint64_t userId, const std::string& instanceId) { + json body; + body["user_id"] = userId; + + http_->post(buildPath("/instances/" + instanceId + "/offline"), body); +} + +json Client::getConstants() { + return http_->get(buildPath("/constants"))["data"]; +} + +json Client::getConstant(const std::string& key) { + return http_->get(buildPath("/constants/" + key))["data"]; +} + +std::vector Client::downloadConstant(const std::string& key) { + return http_->download(buildPath("/constants/" + key + "/download")); +} + +json Client::getVariables() { + return http_->get(buildPath("/variables"))["data"]; +} + +json Client::getVariable(const std::string& key) { + return http_->get(buildPath("/variables/" + key))["data"]; +} + +std::vector Client::downloadVariable(const std::string& key) { + return http_->download(buildPath("/variables/" + key + "/download")); +} + +json Client::uploadVariable(const std::string& key, const std::string& filePath) { + return http_->postForm(buildPath("/variables/" + key + "/upload"), {}, {{"file", filePath}})["data"]; +} + +void Client::updateVariables(const std::map& variables) { + json body; + body["variables"] = variables; + http_->post(buildPath("/variables"), body); +} + +json Client::createVariableRecord(const std::string& key, const json& data) { + return http_->post(buildPath("/variables/" + key + "/records"), data)["data"]; +} + +json Client::getVariableRecords(const std::string& key, int page, int pageSize) { + std::string path = buildPath("/variables/" + key + "/records") + + "?page=" + std::to_string(page) + "&page_size=" + std::to_string(pageSize); + return http_->get(path)["data"]; +} + +void Client::deleteVariableRecord(const std::string& key, uint64_t recordId) { + http_->get(buildPath("/variables/" + key + "/records/" + std::to_string(recordId))); +} + +json Client::executeDynamicCode(const std::string& key, const json& params, uint64_t targetUserId) { + json body; + body["params"] = params; + if (targetUserId > 0) { + body["user_id"] = targetUserId; + } + + return http_->post(buildPath("/dynamic-code/" + key + "/execute"), body)["data"]; +} + +} // namespace verify diff --git a/sdk/cpp/verify_client.hpp b/sdk/cpp/verify_client.hpp new file mode 100644 index 0000000..a39cb4d --- /dev/null +++ b/sdk/cpp/verify_client.hpp @@ -0,0 +1,230 @@ +#ifndef VERIFY_CLIENT_HPP +#define VERIFY_CLIENT_HPP + +#include +#include +#include +#include +#include + +#ifdef USE_OPENSSL +#include +#include +#endif + +#ifdef USE_SYSTEM_CURL +#include +#else +// 简化的CURL模拟 - 用于测试 +#endif + +#include "json.hpp" + +namespace verify { + +using json = nlohmann::json; + +class CryptoException : public std::exception { +public: + explicit CryptoException(const std::string& msg) : message_(msg) {} + const char* what() const noexcept override { return message_.c_str(); } +private: + std::string message_; +}; + +class ApiException : public std::exception { +public: + ApiException(int code, const std::string& msg) : code_(code), message_(msg) {} + int code() const { return code_; } + const char* what() const noexcept override { return message_.c_str(); } +private: + int code_; + std::string message_; +}; + +enum class EncryptType { + None, + AES, + RC4 +}; + +class Crypto { +public: + Crypto(EncryptType type, const std::string& key); + ~Crypto(); + + std::string encrypt(const std::string& plaintext); + std::string decrypt(const std::string& ciphertext); + +private: + EncryptType type_; + std::string key_; + + std::string encryptAES(const std::string& plaintext); + std::string decryptAES(const std::string& ciphertext); + std::string encryptRC4(const std::string& plaintext); + std::string decryptRC4(const std::string& ciphertext); + std::vector padKey(const std::vector& key, size_t targetLen); +}; + +class HttpClient { +public: + HttpClient(); + ~HttpClient(); + + void setBaseUrl(const std::string& url); + void setTimeout(int seconds); + void addHeader(const std::string& key, const std::string& value); + void clearHeaders(); + + json get(const std::string& path); + json post(const std::string& path, const json& body); + json postForm(const std::string& path, const std::map& fields, const std::map& files = {}); + + std::vector download(const std::string& path); + + void setCrypto(std::shared_ptr crypto); + +private: + std::string baseUrl_; + int timeout_ = 30; + std::map headers_; + std::shared_ptr crypto_; + + static size_t writeCallback(void* contents, size_t size, size_t nmemb, std::string* userp); + static size_t writeBinaryCallback(void* contents, size_t size, size_t nmemb, std::vector* userp); + + std::string buildUrl(const std::string& path); + json processResponse(const std::string& response); +}; + +struct AppConfig { + uint64_t id; + std::string name; + std::string description; + std::string iconUrl; + std::string status; + std::string billingType; + std::string loginPolicy; + int maxDevices; + std::string multiOpenMode; + int maxInstances; + bool enableTrial; + double trialBalance; + int heartbeatInterval; + int heartbeatTimeout; +}; + +struct UserInfo { + uint64_t userId; + std::string username; + double balance; + std::string status; +}; + +struct DeviceInfo { + uint64_t id; + std::string deviceId; + std::string deviceName; + std::string deviceType; + std::string status; + int onlineSessions; + std::string createdAt; +}; + +struct VersionInfo { + bool hasUpdate; + std::string latestVersion; + std::string downloadUrl; + int64_t fileSize; + std::string fileHash; + std::string entryFile; + std::string updateNotes; + std::string updateStrategy; + std::string updateType; + std::string updateMethod; +}; + +class Client { +public: + Client(const std::string& baseUrl, const std::string& appKey); + ~Client(); + + void setToken(const std::string& token); + void setEncryptType(EncryptType type, const std::string& key); + void setTimeout(int seconds); + + // Application info + AppConfig getAppInfo(); + VersionInfo checkUpdate(const std::string& version = ""); + std::vector getAnnouncements(); + + // User authentication + uint64_t registerUser(const std::string& username, + const std::string& password, + const std::string& deviceId, + const std::string& deviceName = "", + const std::string& deviceType = "windows", + const std::string& instanceId = "", + const std::string& email = "", + const std::string& emailCode = ""); + + json login(const std::string& username, + const std::string& password, + const std::string& deviceId, + const std::string& deviceName = "", + const std::string& deviceType = "windows", + const std::string& instanceId = ""); + + void sendEmailCode(const std::string& email, const std::string& purpose = "register"); + void resetPassword(const std::string& email, const std::string& code, const std::string& newPassword); + void changePassword(const std::string& username, const std::string& oldPassword, const std::string& newPassword); + + // Account + UserInfo getAccount(uint64_t userId); + json heartbeat(uint64_t userId, const std::string& deviceId, const std::string& instanceId = ""); + + // Recharge + json recharge(const std::string& username, const std::string& cardKey, const std::string& deviceId); + json trial(uint64_t userId); + + // Device management + std::vector getDevices(); + json getDeviceCount(uint64_t userId); + void unbindDevice(uint64_t userId, const std::string& deviceId); + void unbindDeviceWithAuth(const std::string& username, const std::string& password, const std::string& deviceId); + std::vector getInstances(uint64_t userId); + void forceOfflineInstance(uint64_t userId, const std::string& instanceId); + + // Cloud data + json getConstants(); + json getConstant(const std::string& key); + std::vector downloadConstant(const std::string& key); + + json getVariables(); + json getVariable(const std::string& key); + std::vector downloadVariable(const std::string& key); + json uploadVariable(const std::string& key, const std::string& filePath); + void updateVariables(const std::map& variables); + + json createVariableRecord(const std::string& key, const json& data); + json getVariableRecords(const std::string& key, int page = 1, int pageSize = 20); + void deleteVariableRecord(const std::string& key, uint64_t recordId); + + // Dynamic code + json executeDynamicCode(const std::string& key, const json& params, uint64_t targetUserId = 0); + +private: + std::string baseUrl_; + std::string appKey_; + std::string token_; + std::shared_ptr http_; + std::shared_ptr crypto_; + EncryptType encryptType_ = EncryptType::None; + + std::string buildPath(const std::string& endpoint); +}; + +} // namespace verify + +#endif // VERIFY_CLIENT_HPP