ed4a561704
- 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 <noreply@anthropic.com>
831 lines
26 KiB
C++
831 lines
26 KiB
C++
#include "verify_client.hpp"
|
|
#include <sstream>
|
|
#include <fstream>
|
|
#include <stdexcept>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <random>
|
|
#include <algorithm>
|
|
|
|
#ifdef USE_OPENSSL
|
|
#include <openssl/bio.h>
|
|
#include <openssl/buffer.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/rand.h>
|
|
#endif
|
|
|
|
namespace verify {
|
|
|
|
// ============================================================================
|
|
// Crypto Implementation
|
|
// ============================================================================
|
|
|
|
Crypto::Crypto(EncryptType type, const std::string& key)
|
|
: type_(type), key_(key) {
|
|
}
|
|
|
|
Crypto::~Crypto() = default;
|
|
|
|
std::vector<uint8_t> Crypto::padKey(const std::vector<uint8_t>& key, size_t targetLen) {
|
|
if (key.size() >= targetLen) {
|
|
return std::vector<uint8_t>(key.begin(), key.begin() + targetLen);
|
|
}
|
|
|
|
std::vector<uint8_t> 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<uint8_t>& 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<int>(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<uint8_t> base64Decode(const std::string& encoded) {
|
|
#ifdef USE_OPENSSL
|
|
BIO* bio = BIO_new_mem_buf(encoded.data(), static_cast<int>(encoded.size()));
|
|
BIO* b64 = BIO_new(BIO_f_base64());
|
|
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
|
|
BIO_push(b64, bio);
|
|
|
|
std::vector<uint8_t> result(encoded.size());
|
|
int len = BIO_read(b64, result.data(), static_cast<int>(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<uint8_t> result;
|
|
result.reserve(encoded.size() * 3 / 4);
|
|
|
|
int val = 0, bits = 0;
|
|
for (char c : encoded) {
|
|
if (c == '=') break;
|
|
int v = table[static_cast<uint8_t>(c)];
|
|
if (v < 0) continue;
|
|
|
|
val = (val << 6) | v;
|
|
bits += 6;
|
|
|
|
if (bits >= 8) {
|
|
bits -= 8;
|
|
result.push_back(static_cast<uint8_t>((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<uint8_t> keyBytes = padKey(std::vector<uint8_t>(key_.begin(), key_.end()), 32);
|
|
|
|
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
|
|
if (!ctx) throw CryptoException("Failed to create cipher context");
|
|
|
|
std::vector<uint8_t> 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<uint8_t> ciphertext(plaintext.size() + 16);
|
|
int len;
|
|
EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
|
|
reinterpret_cast<const uint8_t*>(plaintext.data()), plaintext.size());
|
|
|
|
int ciphertextLen = len;
|
|
EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len);
|
|
ciphertextLen += len;
|
|
|
|
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> data = base64Decode(ciphertext);
|
|
if (data.size() < 28) throw CryptoException("Ciphertext too short");
|
|
|
|
std::vector<uint8_t> keyBytes = padKey(std::vector<uint8_t>(key_.begin(), key_.end()), 32);
|
|
|
|
std::vector<uint8_t> nonce(data.begin(), data.begin() + 12);
|
|
std::vector<uint8_t> tag(data.end() - 16, data.end());
|
|
std::vector<uint8_t> 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<uint8_t> 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<uint8_t>& key) {
|
|
for (int i = 0; i < 256; ++i) s_[i] = static_cast<uint8_t>(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<uint8_t>& 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<uint8_t> keyBytes(key_.begin(), key_.end());
|
|
if (keyBytes.empty()) throw CryptoException("RC4 key cannot be empty");
|
|
|
|
std::vector<uint8_t> data(plaintext.begin(), plaintext.end());
|
|
RC4 rc4(keyBytes);
|
|
rc4.process(data);
|
|
|
|
return base64Encode(data);
|
|
}
|
|
|
|
std::string Crypto::decryptRC4(const std::string& ciphertext) {
|
|
std::vector<uint8_t> keyBytes(key_.begin(), key_.end());
|
|
if (keyBytes.empty()) throw CryptoException("RC4 key cannot be empty");
|
|
|
|
std::vector<uint8_t> 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_ = 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<char*>(contents), totalSize);
|
|
return totalSize;
|
|
}
|
|
|
|
size_t HttpClient::writeBinaryCallback(void* contents, size_t size, size_t nmemb, std::vector<uint8_t>* userp) {
|
|
size_t totalSize = size * nmemb;
|
|
userp->insert(userp->end(), static_cast<uint8_t*>(contents), static_cast<uint8_t*>(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<std::string>()));
|
|
}
|
|
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<int>() != 200) {
|
|
throw ApiException(j["code"].get<int>(), j["message"].get<std::string>());
|
|
}
|
|
|
|
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<int>() != 200) {
|
|
throw ApiException(j["code"].get<int>(), j["message"].get<std::string>());
|
|
}
|
|
|
|
return j;
|
|
}
|
|
|
|
json HttpClient::postForm(const std::string& path, const std::map<std::string, std::string>& fields, const std::map<std::string, std::string>& 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<int>() != 200) {
|
|
throw ApiException(j["code"].get<int>(), j["message"].get<std::string>());
|
|
}
|
|
|
|
return j;
|
|
}
|
|
|
|
std::vector<uint8_t> HttpClient::download(const std::string& path) {
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) throw std::runtime_error("Failed to initialize CURL");
|
|
|
|
std::vector<uint8_t> 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<HttpClient>();
|
|
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<Crypto>(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<uint64_t>();
|
|
config.name = j["data"]["name"].get<std::string>();
|
|
config.description = j["data"]["description"].get<std::string>();
|
|
config.iconUrl = j["data"]["icon_url"].get<std::string>();
|
|
config.status = j["data"]["status"].get<std::string>();
|
|
config.billingType = j["data"]["billing_type"].get<std::string>();
|
|
config.loginPolicy = j["data"]["login_policy"].get<std::string>();
|
|
config.maxDevices = j["data"]["max_devices"].get<int>();
|
|
config.multiOpenMode = j["data"]["multi_open_mode"].get<std::string>();
|
|
config.maxInstances = j["data"]["max_instances"].get<int>();
|
|
config.enableTrial = j["data"]["enable_trial"].get<bool>();
|
|
config.trialBalance = j["data"]["trial_balance"].get<double>();
|
|
config.heartbeatInterval = j["data"]["heartbeat_interval"].get<int>();
|
|
config.heartbeatTimeout = j["data"]["heartbeat_timeout"].get<int>();
|
|
|
|
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<bool>();
|
|
info.latestVersion = j["data"]["latest_version"].get<std::string>();
|
|
info.downloadUrl = j["data"]["download_url"].get<std::string>();
|
|
info.fileSize = j["data"]["file_size"].get<int64_t>();
|
|
info.fileHash = j["data"]["file_hash"].get<std::string>();
|
|
info.entryFile = j["data"]["entry_file"].get<std::string>();
|
|
info.updateNotes = j["data"]["update_notes"].get<std::string>();
|
|
info.updateStrategy = j["data"]["update_strategy"].get<std::string>();
|
|
info.updateType = j["data"]["update_type"].get<std::string>();
|
|
info.updateMethod = j["data"]["update_method"].get<std::string>();
|
|
|
|
return info;
|
|
}
|
|
|
|
std::vector<json> Client::getAnnouncements() {
|
|
auto j = http_->get(buildPath("/announcements"));
|
|
return j["data"].get<std::vector<json>>();
|
|
}
|
|
|
|
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<uint64_t>();
|
|
}
|
|
|
|
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<std::string>());
|
|
}
|
|
|
|
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<uint64_t>();
|
|
info.username = j["data"]["username"].get<std::string>();
|
|
info.balance = j["data"]["balance"].get<double>();
|
|
info.status = j["data"]["status"].get<std::string>();
|
|
|
|
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<DeviceInfo> Client::getDevices() {
|
|
auto j = http_->get(buildPath("/devices"));
|
|
|
|
std::vector<DeviceInfo> devices;
|
|
for (const auto& d : j["data"]) {
|
|
DeviceInfo info;
|
|
info.id = d["id"].get<uint64_t>();
|
|
info.deviceId = d["device_id"].get<std::string>();
|
|
info.deviceName = d["device_name"].get<std::string>();
|
|
info.deviceType = d["device_type"].get<std::string>();
|
|
info.status = d["status"].get<std::string>();
|
|
info.onlineSessions = d["online_sessions"].get<int>();
|
|
info.createdAt = d["created_at"].get<std::string>();
|
|
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<json> Client::getInstances(uint64_t userId) {
|
|
json body;
|
|
body["user_id"] = userId;
|
|
|
|
return http_->post(buildPath("/instances"), body)["data"].get<std::vector<json>>();
|
|
}
|
|
|
|
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<uint8_t> 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<uint8_t> 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<std::string, std::string>& 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
|