| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- #include "acme.h"
- #include "../utils.h"
- #include <cctype>
- #include <cstdlib>
- #include <iostream>
- #include <sstream>
- 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<unsigned char>(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<std::string>& 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
|