#include "acme.h" #include "../utils.h" #include #include #include #include namespace ngs { namespace ssl { namespace { bool looks_like_ip(const std::string& d) { if (d.empty()) { return false; } bool digit_or_dot = true; for (char c : d) { if (!(std::isdigit(static_cast(c)) || c == '.')) { digit_or_dot = false; break; } } if (digit_or_dot && d.find('.') != std::string::npos) { return true; } return d.find(':') != std::string::npos; // IPv6-ish } } // namespace std::string acme_webroot() { return join_path(www_root(), "acme"); } std::string acme_home() { return join_path(software_root(), "acme"); } std::string acme_bin() { return join_path(acme_home(), "acme.sh"); } bool ensure_acme_client(const std::string& email, std::string& err) { if (!ensure_dir(acme_home())) { err = "无法创建 acme 目录: " + acme_home(); return false; } if (!ensure_dir(acme_webroot())) { err = "无法创建 ACME webroot: " + acme_webroot(); return false; } if (path_exists(acme_bin())) { return true; } std::cout << "正在安装 acme.sh (Let's Encrypt 客户端)...\n"; log_info("installing acme.sh home=" + acme_home()); const std::string mail = email.empty() ? "noreply@localhost" : email; // Prefer China mirrors (get.acme.sh / GitHub is often very slow). // See: https://github.com/acmesh-official/acme.sh/wiki/Install-in-China const std::string src_dir = join_path(software_root(), ".build/acme.sh-src"); remove_path(src_dir); ensure_dir(join_path(software_root(), ".build")); bool got_src = false; const char* git_mirrors[] = { "https://gitee.com/neilpang/acme.sh.git", "https://gitee.com/acmesh-official/acme.sh.git", "https://github.com/acmesh-official/acme.sh.git", }; for (const char* url : git_mirrors) { log_info(std::string("acme.sh clone ") + url); const std::string cmd = "git clone --depth 1 \"" + std::string(url) + "\" \"" + src_dir + "\""; if (run_cmd(cmd, true) == 0 && path_exists(join_path(src_dir, "acme.sh"))) { got_src = true; break; } remove_path(src_dir); } if (!got_src) { // Last resort: official installer (may be slow outside China mirrors). log_info("acme.sh fallback: get.acme.sh"); const std::string cmd = "curl -fsSL --connect-timeout 15 --max-time 180 " "https://get.acme.sh | sh -s email=" + mail + " --home \"" + acme_home() + "\" --nocron"; if (run_cmd(cmd, true) != 0 || !path_exists(acme_bin())) { err = "安装 acme.sh 失败,请检查网络(建议可访问 gitee.com)"; return false; } log_info("acme.sh installed"); return true; } const std::string install = "cd \"" + src_dir + "\" && ./acme.sh --install -m \"" + mail + "\" --home \"" + acme_home() + "\" --nocron"; if (run_cmd(install, true) != 0 || !path_exists(acme_bin())) { err = "安装 acme.sh 失败"; return false; } log_info("acme.sh installed"); return true; } bool issue_letsencrypt(const std::vector& domains, const std::string& email, const std::string& cert_file, const std::string& key_file, std::string& err) { if (domains.empty()) { err = "域名列表为空"; return false; } for (const auto& d : domains) { if (looks_like_ip(d) || d == "localhost" || d.find("localhost.") == 0) { err = "Let's Encrypt 不支持 IP / localhost 域名: " + d; return false; } } if (!ensure_acme_client(email, err)) { return false; } if (!ensure_dir(acme_webroot())) { err = "无法创建 ACME webroot"; return false; } // Prefer Let's Encrypt production CA. std::ostringstream issue; issue << "\"" << acme_bin() << "\" --home \"" << acme_home() << "\"" << " --issue --server letsencrypt"; if (!email.empty()) { issue << " --accountemail \"" << email << "\""; } for (const auto& d : domains) { issue << " -d \"" << d << "\""; } issue << " -w \"" << acme_webroot() << "\" --force"; std::cout << "申请证书: "; for (size_t i = 0; i < domains.size(); ++i) { if (i) { std::cout << ", "; } std::cout << domains[i]; } std::cout << "\n"; log_info("acme issue domains=" + domains.front() + " count=" + std::to_string(domains.size())); if (run_cmd(issue.str(), true) != 0) { err = "证书申请失败。请确认:域名已解析到本机、80 端口可从公网访问、" "Nginx 已正确提供 ACME 校验路径"; return false; } const std::string primary = domains.front(); std::ostringstream install; install << "\"" << acme_bin() << "\" --home \"" << acme_home() << "\"" << " --install-cert -d \"" << primary << "\"" << " --fullchain-file \"" << cert_file << "\"" << " --key-file \"" << key_file << "\"" << " --reloadcmd \"true\""; if (run_cmd(install.str(), true) != 0) { err = "证书已签发但安装到站点目录失败"; return false; } if (!path_exists(cert_file) || !path_exists(key_file)) { err = "证书文件未生成"; return false; } log_info("acme cert installed cert=" + cert_file); return true; } } // namespace ssl } // namespace ngs