acme.cpp 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #include "acme.h"
  2. #include "../utils.h"
  3. #include <cctype>
  4. #include <cstdlib>
  5. #include <iostream>
  6. #include <sstream>
  7. namespace ngs {
  8. namespace ssl {
  9. namespace {
  10. bool looks_like_ip(const std::string& d) {
  11. if (d.empty()) {
  12. return false;
  13. }
  14. bool digit_or_dot = true;
  15. for (char c : d) {
  16. if (!(std::isdigit(static_cast<unsigned char>(c)) || c == '.')) {
  17. digit_or_dot = false;
  18. break;
  19. }
  20. }
  21. if (digit_or_dot && d.find('.') != std::string::npos) {
  22. return true;
  23. }
  24. return d.find(':') != std::string::npos; // IPv6-ish
  25. }
  26. } // namespace
  27. std::string acme_webroot() {
  28. return join_path(www_root(), "acme");
  29. }
  30. std::string acme_home() {
  31. return join_path(software_root(), "acme");
  32. }
  33. std::string acme_bin() {
  34. return join_path(acme_home(), "acme.sh");
  35. }
  36. bool ensure_acme_client(const std::string& email, std::string& err) {
  37. if (!ensure_dir(acme_home())) {
  38. err = "无法创建 acme 目录: " + acme_home();
  39. return false;
  40. }
  41. if (!ensure_dir(acme_webroot())) {
  42. err = "无法创建 ACME webroot: " + acme_webroot();
  43. return false;
  44. }
  45. if (path_exists(acme_bin())) {
  46. return true;
  47. }
  48. std::cout << "正在安装 acme.sh (Let's Encrypt 客户端)...\n";
  49. log_info("installing acme.sh home=" + acme_home());
  50. const std::string mail =
  51. email.empty() ? "noreply@localhost" : email;
  52. // Prefer China mirrors (get.acme.sh / GitHub is often very slow).
  53. // See: https://github.com/acmesh-official/acme.sh/wiki/Install-in-China
  54. const std::string src_dir =
  55. join_path(software_root(), ".build/acme.sh-src");
  56. remove_path(src_dir);
  57. ensure_dir(join_path(software_root(), ".build"));
  58. bool got_src = false;
  59. const char* git_mirrors[] = {
  60. "https://gitee.com/neilpang/acme.sh.git",
  61. "https://gitee.com/acmesh-official/acme.sh.git",
  62. "https://github.com/acmesh-official/acme.sh.git",
  63. };
  64. for (const char* url : git_mirrors) {
  65. log_info(std::string("acme.sh clone ") + url);
  66. const std::string cmd =
  67. "git clone --depth 1 \"" + std::string(url) + "\" \"" + src_dir +
  68. "\"";
  69. if (run_cmd(cmd, true) == 0 &&
  70. path_exists(join_path(src_dir, "acme.sh"))) {
  71. got_src = true;
  72. break;
  73. }
  74. remove_path(src_dir);
  75. }
  76. if (!got_src) {
  77. // Last resort: official installer (may be slow outside China mirrors).
  78. log_info("acme.sh fallback: get.acme.sh");
  79. const std::string cmd =
  80. "curl -fsSL --connect-timeout 15 --max-time 180 "
  81. "https://get.acme.sh | sh -s email=" +
  82. mail + " --home \"" + acme_home() + "\" --nocron";
  83. if (run_cmd(cmd, true) != 0 || !path_exists(acme_bin())) {
  84. err = "安装 acme.sh 失败,请检查网络(建议可访问 gitee.com)";
  85. return false;
  86. }
  87. log_info("acme.sh installed");
  88. return true;
  89. }
  90. const std::string install =
  91. "cd \"" + src_dir + "\" && ./acme.sh --install -m \"" + mail +
  92. "\" --home \"" + acme_home() + "\" --nocron";
  93. if (run_cmd(install, true) != 0 || !path_exists(acme_bin())) {
  94. err = "安装 acme.sh 失败";
  95. return false;
  96. }
  97. log_info("acme.sh installed");
  98. return true;
  99. }
  100. bool issue_letsencrypt(const std::vector<std::string>& domains,
  101. const std::string& email,
  102. const std::string& cert_file,
  103. const std::string& key_file, std::string& err) {
  104. if (domains.empty()) {
  105. err = "域名列表为空";
  106. return false;
  107. }
  108. for (const auto& d : domains) {
  109. if (looks_like_ip(d) || d == "localhost" || d.find("localhost.") == 0) {
  110. err = "Let's Encrypt 不支持 IP / localhost 域名: " + d;
  111. return false;
  112. }
  113. }
  114. if (!ensure_acme_client(email, err)) {
  115. return false;
  116. }
  117. if (!ensure_dir(acme_webroot())) {
  118. err = "无法创建 ACME webroot";
  119. return false;
  120. }
  121. // Prefer Let's Encrypt production CA.
  122. std::ostringstream issue;
  123. issue << "\"" << acme_bin() << "\" --home \"" << acme_home() << "\""
  124. << " --issue --server letsencrypt";
  125. if (!email.empty()) {
  126. issue << " --accountemail \"" << email << "\"";
  127. }
  128. for (const auto& d : domains) {
  129. issue << " -d \"" << d << "\"";
  130. }
  131. issue << " -w \"" << acme_webroot() << "\" --force";
  132. std::cout << "申请证书: ";
  133. for (size_t i = 0; i < domains.size(); ++i) {
  134. if (i) {
  135. std::cout << ", ";
  136. }
  137. std::cout << domains[i];
  138. }
  139. std::cout << "\n";
  140. log_info("acme issue domains=" + domains.front() +
  141. " count=" + std::to_string(domains.size()));
  142. if (run_cmd(issue.str(), true) != 0) {
  143. err = "证书申请失败。请确认:域名已解析到本机、80 端口可从公网访问、"
  144. "Nginx 已正确提供 ACME 校验路径";
  145. return false;
  146. }
  147. const std::string primary = domains.front();
  148. std::ostringstream install;
  149. install << "\"" << acme_bin() << "\" --home \"" << acme_home() << "\""
  150. << " --install-cert -d \"" << primary << "\""
  151. << " --fullchain-file \"" << cert_file << "\""
  152. << " --key-file \"" << key_file << "\""
  153. << " --reloadcmd \"true\"";
  154. if (run_cmd(install.str(), true) != 0) {
  155. err = "证书已签发但安装到站点目录失败";
  156. return false;
  157. }
  158. if (!path_exists(cert_file) || !path_exists(key_file)) {
  159. err = "证书文件未生成";
  160. return false;
  161. }
  162. log_info("acme cert installed cert=" + cert_file);
  163. return true;
  164. }
  165. } // namespace ssl
  166. } // namespace ngs