#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