* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "remote_deployer.h"
#include <sys/stat.h>
#include <utility>
#include "common/constants/constants.h"
#include "common/logs/logging.h"
#include "common/metrics/metrics_adapter.h"
#include "common/utils/exec_utils.h"
#include "common/utils/ssl_config.h"
#include "minizip/unzip.h"
#include "utils/os_utils.hpp"
#include "common/constants.h"
namespace functionsystem::function_agent {
const int SHA256_VALUE_LEN = 32;
const int SHA512_VALUE_LEN = 64;
const int CHAR_TO_HEX_LEN = 2;
const size_t UNZIPINFO_HEADER_LEN = 2;
const size_t CMD_OUTPUT_MAX_LEN = 1024 * 1024 * 10;
const uint32_t ZIP_FILE_BUFFER = 200;
RemoteDeployer::RemoteDeployer(messages::CodePackageThresholds codePackageThresholds, bool enableSignatureValidation)
: codePackageThresholds_(std::move(codePackageThresholds)), enableSignatureValidation_(enableSignatureValidation)
{
unzipFileSizeMaxBytes_ = static_cast<uint64_t>(codePackageThresholds_.unzipfilesizemaxmb()) * SIZE_MEGA_BYTES;
if (unzipFileSizeMaxBytes_ / SIZE_MEGA_BYTES !=
static_cast<uint64_t>(codePackageThresholds_.unzipfilesizemaxmb())) {
unzipFileSizeMaxBytes_ = function_agent::DEFAULT_UNZIP_FILE_LIMIT_SIZE_MB;
}
}
std::string RemoteDeployer::GetDestination(const std::string &deployDir, const std::string &bucketID,
const std::string &objectID)
{
std::string layerDir = litebus::os::Join(deployDir, "layer");
std::string funcDir = litebus::os::Join(layerDir, "func");
std::string bucketDir = litebus::os::Join(funcDir, bucketID);
return litebus::os::Join(bucketDir, TransMultiLevelDirToSingle(objectID));
}
bool RemoteDeployer::IsDeployed(const std::string &destination, bool isMonopoly)
{
if (!litebus::os::ExistPath(destination)) {
return false;
}
if (!isMonopoly) {
return true;
}
auto option = litebus::os::Ls(destination);
if (option.IsSome() && !option.Get().empty()) {
return true;
}
return false;
}
void RemoteDeployer::CollectFunctionInfo(const std::string &destFile,
const std::shared_ptr<messages::DeployRequest> &request)
{
const std::string &objectId = request->mutable_deploymentconfig()->objectid();
struct stat statBuf {};
if (stat(destFile.c_str(), &statBuf) == -1) {
YRLOG_ERROR("failed to get function package info for object({}) with the path({})", objectId, destFile);
return;
}
struct functionsystem::metrics::MeterData data {
static_cast<double>(statBuf.st_size),
{
{ "instanceId", request->instanceid() },
{
"objectId", objectId
}
}
};
struct functionsystem::metrics::MeterTitle functionPackageTitle {
"yr_app_user_code_pkg_size", "function code package size in node", "Byte"
};
functionsystem::metrics::MetricsAdapter::GetInstance().ReportGauge(functionPackageTitle, data);
}
std::string RemoteDeployer::GetFileNameFromObjectID(const std::string &objectID)
{
auto items = litebus::strings::Split(objectID, "/");
if (items.empty()) {
return objectID;
}
return items[items.size() - 1];
}
DeployResult RemoteDeployer::Deploy(const std::shared_ptr<messages::DeployRequest> &request)
{
const ::messages::DeploymentConfig &config = request->deploymentconfig();
YRLOG_DEBUG("remote deployer received Deploy request to directory {}, bucketID {} , objectID {}",
config.deploydir(), config.bucketid(), config.objectid());
DeployResult result;
result.destination = config.deploydir();
if (config.bucketid().empty() && config.objectid().empty()) {
YRLOG_WARN("bucketID and objectID is empty, skip download");
result.status = Status::OK();
return result;
}
std::string objectDir = GetDestination(config.deploydir(), config.bucketid(), config.objectid());
if (request->schedpolicyname() != MONOPOLY_SCHEDULE) {
result.destination = objectDir;
}
std::string objectTmpDir = objectDir + "-tmp";
if (!CheckIllegalChars(result.destination) || !CheckIllegalChars(objectTmpDir) ||
!litebus::os::Mkdir(objectTmpDir).IsNone() || !litebus::os::Mkdir(result.destination).IsNone()) {
YRLOG_ERROR("failed to create dir for object({}).", config.objectid());
result.status =
Status(StatusCode::FUNC_AGENT_OBS_OPEN_FILE_ERROR,
"failed to create dir for object(" + config.objectid() + "), msg: +" + litebus::os::Strerror(errno));
return result;
}
auto tmpDirRealpathOption = litebus::os::RealPath(objectTmpDir);
if (tmpDirRealpathOption.IsNone()) {
(void)litebus::os::Rmdir(objectTmpDir);
YRLOG_ERROR("failed to get realpath of tmp dir for object({}).", config.objectid());
result.status = Status(StatusCode::FUNC_AGENT_OBS_OPEN_FILE_ERROR,
"failed to create dir for object(" + config.objectid() + ").");
return result;
}
const auto &objectTmpDirRealpath = tmpDirRealpathOption.Get();
std::string objectFile = litebus::os::Join(objectTmpDirRealpath, GetFileNameFromObjectID(config.objectid()));
Status downloadStatus = DownloadCode(objectFile, config);
YRLOG_DEBUG(
"codePackageThresholds: filecountsmax({}), zipfilesizemaxmb({}), unzipfilesizemaxmb({}), dirdepthmax({})",
codePackageThresholds_.filecountsmax(), codePackageThresholds_.zipfilesizemaxmb(),
codePackageThresholds_.unzipfilesizemaxmb(), codePackageThresholds_.dirdepthmax());
if (downloadStatus.IsError()) {
(void)litebus::os::Rmdir(objectTmpDirRealpath);
result.status = downloadStatus;
return result;
}
Status contentStatus = PackageValidation(objectFile, config.sha512(), config.sha256());
if (contentStatus.IsError()) {
(void)litebus::os::Rmdir(objectTmpDirRealpath);
result.status = contentStatus;
return result;
}
CollectFunctionInfo(objectFile, request);
Status unzipStatus = UnzipFile(result.destination, objectFile);
if (unzipStatus.IsError()) {
YRLOG_ERROR("failed to unzip code for object({}).", config.objectid());
(void)litebus::os::Rmdir(objectTmpDirRealpath);
result.status = unzipStatus;
return result;
}
std::string cmd = "chmod -R 750 " + result.destination;
if (auto code(std::system(cmd.c_str())); code) {
YRLOG_WARN("failed to execute chmod cmd({}). code: {}", cmd, code);
}
(void)litebus::os::Rmdir(objectTmpDirRealpath);
result.status = Status::OK();
return result;
}
bool RemoteDeployer::Clear(const std::string &filePath, const std::string &objectKey)
{
return ClearFile(filePath, objectKey);
}
Status RemoteDeployer::UnzipFile(const std::string &destDir, const std::string &destFile)
{
std::string cmd = "unzip -d " + destDir + " " + destFile;
if (!CheckIllegalChars(cmd)) {
return Status(StatusCode::PARAMETER_ERROR, "command has invalid characters");
}
if (auto code(std::system(cmd.c_str())); code) {
YRLOG_ERROR("failed to execute unzip cmd({}). code: {}", cmd, code);
return Status(StatusCode::FUNC_AGENT_OBS_OPEN_FILE_ERROR, "failed to unzip file");
}
(void)litebus::os::Rmdir(destFile);
return Status::OK();
}
Status RemoteDeployer::CheckZipFile(const std::string &path) const
{
unzFile zip = unzOpen64(path.c_str());
if (zip == nullptr) {
return Status(StatusCode::FAILED, "check zip file failed, error: open file " + path + " failed");
}
unz_global_info64 zipInfo{};
if (auto status = unzGetGlobalInfo64(zip, &zipInfo); status != UNZ_OK) {
return Status(StatusCode::FAILED,
"check zip file failed, error: get global info failed, code: " + std::to_string(status));
}
uint64_t sum = 0;
for (ZPOS64_T i = 0; i < zipInfo.number_entry;) {
unz_file_info64 info;
char name[200];
if (auto status = unzGetCurrentFileInfo64(zip, &info, name, 200, nullptr, 0, nullptr, 0); status != UNZ_OK) {
unzClose(zip);
return Status(StatusCode::FAILED, "check zip file failed, error: get file(" + std::string(name) +
") info failed, code: " + std::to_string(status));
}
if (auto status = unzOpenCurrentFile(zip); status != UNZ_OK) {
unzClose(zip);
return Status(StatusCode::FAILED, "check zip file failed, error: open file(" + std::string(name) +
") failed, code: " + std::to_string(status));
}
uint64_t n;
char buf[200];
while ((n = static_cast<uint64_t>(unzReadCurrentFile(zip, buf, ZIP_FILE_BUFFER))) > 0) {
sum += n;
if (sum > unzipFileSizeMaxBytes_) {
unzClose(zip);
return Status(StatusCode::FAILED, "check zip file failed, error: file(" + std::string(name) +
") is bigger than " + std::to_string(unzipFileSizeMaxBytes_));
}
}
if (++i < zipInfo.number_entry) {
if (auto status = unzGoToNextFile(zip); status != UNZ_OK) {
unzClose(zip);
return Status(StatusCode::FAILED,
"check zip file failed, error: go to next file failed, code: " + std::to_string(status));
}
}
}
unzClose(zip);
return Status::OK();
}
Status RemoteDeployer::PackageValidation(const std::string &destFile, const std::string &sha512Str,
const std::string &sha256Str)
{
Status result;
if (enableSignatureValidation_) {
if (!sha512Str.empty()) {
result = CheckSha512(destFile, sha512Str);
} else {
result = CheckSha256(destFile, sha256Str);
}
}
if (result.IsError()) {
return result;
}
result = CheckZipFile(destFile);
if (result.IsError()) {
return result;
}
result = CheckFileContent(destFile);
if (result.IsError()) {
return result;
}
return Status::OK();
}
Status RemoteDeployer::CheckSha256(const std::string &destFile, const std::string &sha256Str)
{
unsigned char sha256Value[SHA256_VALUE_LEN];
auto result = Sha256CalculateFile(destFile.c_str(), sha256Value, SHA256_VALUE_LEN);
if (result != 0) {
YRLOG_ERROR("openssl sha256 failed");
return Status(StatusCode::ERR_USER_CODE_LOAD, "package validation failed, openssl calculate sha256 failed");
}
std::stringstream ss;
for (int i = 0; i < SHA256_VALUE_LEN; i++) {
ss << std::hex << std::setw(CHAR_TO_HEX_LEN) << std::setfill('0') << static_cast<int>(sha256Value[i]);
}
YRLOG_DEBUG("sha256 from request is ({}), sha256 from file calculation is ({})", sha256Str, ss.str());
if (sha256Str.empty() || std::strcmp(sha256Str.c_str(), ss.str().c_str()) != 0) {
YRLOG_ERROR("Check download package validation failed. Signature sha256({}) doesn't match with request ({})",
ss.str(), sha256Str);
return Status(StatusCode::ERR_USER_CODE_LOAD, "package validation failed, package signature doesn't match");
}
return Status::OK();
}
Status RemoteDeployer::CheckSha512(const std::string &destFile, const std::string &sha512Str)
{
unsigned char sha512Value[SHA512_VALUE_LEN];
auto result = Sha512CalculateFile(destFile.c_str(), sha512Value, SHA512_VALUE_LEN);
if (result != 0) {
YRLOG_ERROR("openssl sha512 failed");
return Status(StatusCode::ERR_USER_CODE_LOAD, "package validation failed, openssl calculate sha512 failed");
}
std::stringstream ss;
for (int i = 0; i < SHA512_VALUE_LEN; i++) {
ss << std::hex << std::setw(CHAR_TO_HEX_LEN) << std::setfill('0') << static_cast<int>(sha512Value[i]);
}
YRLOG_DEBUG("sha512 from request is ({}), sha512 from file calculation is ({})", sha512Str, ss.str());
if (sha512Str.empty() || std::strcmp(sha512Str.c_str(), ss.str().c_str()) != 0) {
YRLOG_ERROR("Check download package validation failed. Signature sha512({}) doesn't match with request ({})",
ss.str(), sha512Str);
return Status(StatusCode::ERR_USER_CODE_LOAD, "package validation failed, package signature doesn't match");
}
return Status::OK();
}
Status RemoteDeployer::CheckFileContent(const std::string &destFile)
{
std::vector<std::string> lines;
std::string line;
int32_t fileCount = 0;
uint64_t uncompressedSize = 0;
int32_t depth = 0;
std::string temp;
auto cmdResult = ExecuteCommandByPopen(std::string("unzip -Z -l " + destFile), CMD_OUTPUT_MAX_LEN);
if (cmdResult.empty()) {
return Status(StatusCode::ERR_USER_CODE_LOAD, "package validation failed, unzip command failed");
}
std::stringstream cmdStream(cmdResult);
while (std::getline(cmdStream, line)) {
lines.push_back(line);
}
std::stringstream fileInfoStream(lines.back());
if (fileInfoStream >> fileCount && fileCount >= std::numeric_limits<int32_t>::max()) {
YRLOG_ERROR("Check download package validation failed. Number of files exceeds int32_t numeric limits");
return Status(StatusCode::ERR_USER_CODE_LOAD,
"package validation failed, number of files exceeds int32_t numeric limits");
}
fileInfoStream >> temp;
if (fileInfoStream >> uncompressedSize && uncompressedSize >= std::numeric_limits<uint64_t>::max()) {
YRLOG_ERROR("Check download package validation failed. Unzip file size exceeds uint64_t numeric limits");
return Status(StatusCode::ERR_USER_CODE_LOAD,
"package validation failed, unzip file size exceeds uint64_t numeric limits");
}
for (size_t i = UNZIPINFO_HEADER_LEN; i < lines.size() - 1; i++) {
auto filePath = lines.at(i).substr(lines.at(i).find_last_of(" "), lines.at(i).length());
if (filePath.find("../") != std::string::npos) {
YRLOG_ERROR("Check download package validation failed. Package contains relative path");
return Status(StatusCode::ERR_USER_CODE_LOAD, "package validation failed, package contains relative path");
}
if (filePath.find_last_of("/") == filePath.length() - 1) {
fileCount--;
continue;
}
auto currDepth = std::count(filePath.begin(), filePath.end(), '/');
if (currDepth > depth) {
depth = currDepth;
}
}
if (fileCount > codePackageThresholds_.filecountsmax()) {
YRLOG_ERROR("Check download package validation failed. Number of files({}) exceeds maximum limit({})",
fileCount, codePackageThresholds_.filecountsmax());
return Status(StatusCode::ERR_USER_CODE_LOAD,
"package validation failed, the number of files exceeds maximum limit");
}
if (uncompressedSize > static_cast<uint64_t>(codePackageThresholds_.unzipfilesizemaxmb()) * SIZE_MEGA_BYTES) {
YRLOG_ERROR(
"Check download package validation failed. "
"Unzip file size({} bytes) exceeds maximum limit({} bytes)",
uncompressedSize, static_cast<uint64_t>(codePackageThresholds_.unzipfilesizemaxmb()) * SIZE_MEGA_BYTES);
return Status(StatusCode::ERR_USER_CODE_LOAD,
"package validation failed, unzip file size exceeds maximum limit");
}
if (depth > codePackageThresholds_.dirdepthmax()) {
YRLOG_ERROR("Check download package validation failed. The depth of dir({}) exceeds maximum limit({})", depth,
codePackageThresholds_.dirdepthmax());
return Status(StatusCode::ERR_USER_CODE_LOAD,
"package validation failed, the depth of dir exceeds maximum limit");
}
YRLOG_DEBUG("zipFileInfo: fileCount({}), uncompressedSize({}), dirDepth({})", fileCount, uncompressedSize, depth);
return Status::OK();
}
void RemoteDeployer::UpdateCodePackageThresholds(const messages::CodePackageThresholds &codePackageThresholds)
{
codePackageThresholds_.MergeFrom(codePackageThresholds);
}
}