|
|
@@ -7,8 +7,12 @@
|
|
|
#include "../utils.h"
|
|
|
#include "../weblog/weblog.h"
|
|
|
|
|
|
+#include "util/json.h"
|
|
|
+
|
|
|
#include <algorithm>
|
|
|
+#include <atomic>
|
|
|
#include <cctype>
|
|
|
+#include <chrono>
|
|
|
#include <cstdlib>
|
|
|
#include <cstring>
|
|
|
#include <dirent.h>
|
|
|
@@ -18,6 +22,8 @@
|
|
|
#include <set>
|
|
|
#include <signal.h>
|
|
|
#include <sstream>
|
|
|
+#include <sys/stat.h>
|
|
|
+#include <sys/wait.h>
|
|
|
#include <unistd.h>
|
|
|
#include <vector>
|
|
|
|
|
|
@@ -28,10 +34,15 @@ namespace {
|
|
|
|
|
|
bool write_vhost(const SiteInfo& s);
|
|
|
bool apply_nginx();
|
|
|
-bool start_site_by_name(const std::string& name);
|
|
|
+bool start_site_by_name(const std::string& name, std::string& err);
|
|
|
bool stop_site_by_name(const std::string& name);
|
|
|
-bool restart_site_by_name(const std::string& name);
|
|
|
+bool restart_site_by_name(const std::string& name, std::string& err);
|
|
|
bool find_site_info(const std::string& name, SiteInfo& out);
|
|
|
+bool start_fastweb_process(const SiteInfo& s, std::string& err);
|
|
|
+bool stop_fastweb_process(const SiteInfo& s);
|
|
|
+bool load_url_proxy_rules(const SiteInfo& s, std::vector<UrlProxyRule>& out,
|
|
|
+ std::string& err);
|
|
|
+std::string nginx_escape_comment(std::string s);
|
|
|
|
|
|
std::string vhost_dir() {
|
|
|
return join_path(conf_root(), "vhost");
|
|
|
@@ -212,6 +223,94 @@ std::string site_status_label(const SiteInfo& s) {
|
|
|
return "已停止";
|
|
|
}
|
|
|
|
|
|
+std::string compact_fastweb_error(std::string line) {
|
|
|
+ while (!line.empty() &&
|
|
|
+ (line.back() == '\r' || line.back() == '\n' || line.back() == ' ')) {
|
|
|
+ line.pop_back();
|
|
|
+ }
|
|
|
+ // Prefer a short "module 'x' not found" summary when present.
|
|
|
+ const auto mod = line.find("module '");
|
|
|
+ if (mod != std::string::npos) {
|
|
|
+ const auto end = line.find(" not found", mod);
|
|
|
+ if (end != std::string::npos) {
|
|
|
+ // Keep prefix through "not found"
|
|
|
+ std::string out = line.substr(0, end + 10);
|
|
|
+ if (out.size() > 360) {
|
|
|
+ out = out.substr(out.size() - 360);
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (line.size() > 360) {
|
|
|
+ line.resize(360);
|
|
|
+ line.append("…");
|
|
|
+ }
|
|
|
+ return line;
|
|
|
+}
|
|
|
+
|
|
|
+// Newest YYYYMMDD.log (or any *.log except unrelated) under site log dir.
|
|
|
+std::string newest_fastweb_text_log(const std::string& log_dir) {
|
|
|
+ DIR* dir = opendir(log_dir.c_str());
|
|
|
+ if (!dir) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ std::string best;
|
|
|
+ time_t best_mtime = 0;
|
|
|
+ while (dirent* ent = readdir(dir)) {
|
|
|
+ const std::string name = ent->d_name;
|
|
|
+ if (name.size() < 5 || name[0] == '.') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (name.size() < 4 || name.substr(name.size() - 4) != ".log") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const std::string path = join_path(log_dir, name);
|
|
|
+ struct stat st {};
|
|
|
+ if (stat(path.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (st.st_mtime >= best_mtime) {
|
|
|
+ best_mtime = st.st_mtime;
|
|
|
+ best = path;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ closedir(dir);
|
|
|
+ return best;
|
|
|
+}
|
|
|
+
|
|
|
+std::string fastweb_recent_error(const SiteInfo& s) {
|
|
|
+ const std::string log_dir = join_path(site_dir(s), "log");
|
|
|
+ const std::string path = newest_fastweb_text_log(log_dir);
|
|
|
+ if (path.empty()) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ std::ifstream in(path);
|
|
|
+ if (!in) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ std::vector<std::string> lines;
|
|
|
+ std::string line;
|
|
|
+ while (std::getline(in, line)) {
|
|
|
+ lines.push_back(line);
|
|
|
+ if (lines.size() > 400) {
|
|
|
+ lines.erase(lines.begin(), lines.begin() + 100);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::string best;
|
|
|
+ for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
|
|
|
+ if (it->find("[ERROR]") == std::string::npos &&
|
|
|
+ it->find("start failed") == std::string::npos) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ best = compact_fastweb_error(*it);
|
|
|
+ if (it->find("start failed") != std::string::npos ||
|
|
|
+ it->find("module '") != std::string::npos) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return best;
|
|
|
+}
|
|
|
+
|
|
|
SiteType parse_type(const std::string& key) {
|
|
|
SiteType t = SiteType::Static;
|
|
|
parse_type_key(key, t);
|
|
|
@@ -516,7 +615,8 @@ bool port_used_by_sites(int port, const std::string& except_name = "") {
|
|
|
if (s.listen_port == port) {
|
|
|
return true;
|
|
|
}
|
|
|
- if (s.type == SiteType::Fastweb && !s.upstream.empty()) {
|
|
|
+ if ((s.type == SiteType::Fastweb || s.type == SiteType::Proxy) &&
|
|
|
+ !s.upstream.empty()) {
|
|
|
auto pos = s.upstream.rfind(':');
|
|
|
if (pos != std::string::npos) {
|
|
|
try {
|
|
|
@@ -658,24 +758,81 @@ std::string nginx_static_conf(const SiteInfo& s) {
|
|
|
return out.str();
|
|
|
}
|
|
|
|
|
|
-std::string nginx_proxy_conf(const SiteInfo& s) {
|
|
|
+std::string nginx_proxy_location_block(const std::string& path,
|
|
|
+ const std::string& proxy_pass,
|
|
|
+ const std::string& host,
|
|
|
+ const std::string& comment) {
|
|
|
std::ostringstream out;
|
|
|
- out << "# ngs site: " << s.name << " (proxy)\n"
|
|
|
- << "server {\n"
|
|
|
- << nginx_listen_and_ssl(s)
|
|
|
- << " server_name " << nginx_server_names(s) << ";\n"
|
|
|
- << nginx_acme_location()
|
|
|
- << " location / {\n"
|
|
|
- << " proxy_pass http://" << s.upstream << ";\n"
|
|
|
+ if (!comment.empty()) {
|
|
|
+ out << " # " << comment << "\n";
|
|
|
+ }
|
|
|
+ out << " location " << path << " {\n"
|
|
|
+ << " proxy_pass " << proxy_pass << ";\n"
|
|
|
<< " proxy_http_version 1.1;\n"
|
|
|
- << " proxy_set_header Host $host;\n"
|
|
|
+ << " proxy_set_header Host " << host << ";\n"
|
|
|
<< " proxy_set_header X-Real-IP $remote_addr;\n"
|
|
|
<< " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n"
|
|
|
<< " proxy_set_header X-Forwarded-Proto $scheme;\n"
|
|
|
<< " proxy_set_header Upgrade $http_upgrade;\n"
|
|
|
<< " proxy_set_header Connection \"upgrade\";\n"
|
|
|
- << " }\n"
|
|
|
- << nginx_log_block(s) << "}\n";
|
|
|
+ << " }\n";
|
|
|
+ return out.str();
|
|
|
+}
|
|
|
+
|
|
|
+std::string nginx_url_proxy_pass_target(const UrlProxyRule& r) {
|
|
|
+ if (r.target_type == "unix") {
|
|
|
+ return "http://unix:" + r.target + ":";
|
|
|
+ }
|
|
|
+ return r.target;
|
|
|
+}
|
|
|
+
|
|
|
+std::string nginx_proxy_conf(const SiteInfo& s) {
|
|
|
+ std::vector<UrlProxyRule> rules;
|
|
|
+ std::string load_err;
|
|
|
+ load_url_proxy_rules(s, rules, load_err);
|
|
|
+
|
|
|
+ const UrlProxyRule* root_rule = nullptr;
|
|
|
+ for (const auto& r : rules) {
|
|
|
+ if (r.path == "/") {
|
|
|
+ root_rule = &r;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ std::ostringstream out;
|
|
|
+ out << "# ngs site: " << s.name << " (proxy)\n"
|
|
|
+ << "server {\n"
|
|
|
+ << nginx_listen_and_ssl(s)
|
|
|
+ << " server_name " << nginx_server_names(s) << ";\n"
|
|
|
+ << nginx_acme_location();
|
|
|
+
|
|
|
+ // Sub-path URL proxies first (longer prefixes beat location /).
|
|
|
+ for (const auto& r : rules) {
|
|
|
+ if (r.path == "/") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const std::string comment =
|
|
|
+ r.remark.empty() ? ("url-proxy " + r.id)
|
|
|
+ : ("url-proxy " + nginx_escape_comment(r.remark));
|
|
|
+ out << nginx_proxy_location_block(r.path, nginx_url_proxy_pass_target(r),
|
|
|
+ r.host.empty() ? "$http_host" : r.host,
|
|
|
+ comment);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (root_rule) {
|
|
|
+ const std::string comment =
|
|
|
+ root_rule->remark.empty()
|
|
|
+ ? ("url-proxy " + root_rule->id)
|
|
|
+ : ("url-proxy " + nginx_escape_comment(root_rule->remark));
|
|
|
+ out << nginx_proxy_location_block(
|
|
|
+ "/", nginx_url_proxy_pass_target(*root_rule),
|
|
|
+ root_rule->host.empty() ? "$http_host" : root_rule->host, comment);
|
|
|
+ } else {
|
|
|
+ out << nginx_proxy_location_block("/", "http://" + s.upstream, "$host",
|
|
|
+ "");
|
|
|
+ }
|
|
|
+
|
|
|
+ out << nginx_log_block(s) << "}\n";
|
|
|
return out.str();
|
|
|
}
|
|
|
|
|
|
@@ -688,6 +845,178 @@ bool write_text_file(const std::string& path, const std::string& content) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+std::string url_proxy_dir() {
|
|
|
+ return join_path(conf_root(), "proxy");
|
|
|
+}
|
|
|
+
|
|
|
+std::string url_proxy_file(const SiteInfo& s) {
|
|
|
+ return join_path(url_proxy_dir(), site_id(s) + ".json");
|
|
|
+}
|
|
|
+
|
|
|
+std::string make_proxy_id() {
|
|
|
+ static std::atomic<uint64_t> seq{0};
|
|
|
+ using clock = std::chrono::system_clock;
|
|
|
+ const auto sec = std::chrono::duration_cast<std::chrono::seconds>(
|
|
|
+ clock::now().time_since_epoch())
|
|
|
+ .count();
|
|
|
+ return "up" + std::to_string(sec) + "-" + std::to_string(++seq);
|
|
|
+}
|
|
|
+
|
|
|
+std::string nginx_escape_comment(std::string s) {
|
|
|
+ for (char& c : s) {
|
|
|
+ if (c == '\n' || c == '\r') {
|
|
|
+ c = ' ';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return s;
|
|
|
+}
|
|
|
+
|
|
|
+bool load_url_proxy_rules(const SiteInfo& s, std::vector<UrlProxyRule>& out,
|
|
|
+ std::string& err) {
|
|
|
+ out.clear();
|
|
|
+ const std::string path = url_proxy_file(s);
|
|
|
+ if (!path_exists(path)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ std::ifstream in(path);
|
|
|
+ if (!in) {
|
|
|
+ err = "无法读取 URL 代理配置";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::stringstream buf;
|
|
|
+ buf << in.rdbuf();
|
|
|
+ ylib::json root;
|
|
|
+ if (!root.parse(buf.str())) {
|
|
|
+ err = "URL 代理配置 JSON 无效";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!root.exist("rules") || !root["rules"].is_array()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ auto& arr = root["rules"];
|
|
|
+ for (uint32 i = 0; i < arr.size(); ++i) {
|
|
|
+ const auto& j = arr[i];
|
|
|
+ UrlProxyRule r;
|
|
|
+ r.id = j.exist("id") ? j["id"].to<std::string>() : "";
|
|
|
+ r.path = j.exist("path") ? j["path"].to<std::string>() : "";
|
|
|
+ r.target_type =
|
|
|
+ j.exist("target_type") ? j["target_type"].to<std::string>() : "url";
|
|
|
+ r.target = j.exist("target") ? j["target"].to<std::string>() : "";
|
|
|
+ r.host = j.exist("host") ? j["host"].to<std::string>() : "$http_host";
|
|
|
+ r.remark = j.exist("remark") ? j["remark"].to<std::string>() : "";
|
|
|
+ if (r.id.empty() || r.path.empty() || r.target.empty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ out.push_back(std::move(r));
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool save_url_proxy_rules(const SiteInfo& s,
|
|
|
+ const std::vector<UrlProxyRule>& rules,
|
|
|
+ std::string& err) {
|
|
|
+ if (!ensure_dir(url_proxy_dir())) {
|
|
|
+ err = "无法创建 URL 代理目录";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ ylib::json arr;
|
|
|
+ for (const auto& r : rules) {
|
|
|
+ ylib::json item;
|
|
|
+ item["id"] = r.id;
|
|
|
+ item["path"] = r.path;
|
|
|
+ item["target_type"] = r.target_type;
|
|
|
+ item["target"] = r.target;
|
|
|
+ item["host"] = r.host;
|
|
|
+ item["remark"] = r.remark;
|
|
|
+ arr.push_back(item);
|
|
|
+ }
|
|
|
+ ylib::json root;
|
|
|
+ root["rules"] = arr;
|
|
|
+ if (!write_text_file(url_proxy_file(s), root.to_string(true))) {
|
|
|
+ err = "写入 URL 代理配置失败";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool normalize_url_proxy_rule(UrlProxyRule& rule, std::string& err) {
|
|
|
+ // path
|
|
|
+ while (!rule.path.empty() &&
|
|
|
+ (rule.path.back() == ' ' || rule.path.back() == '\t')) {
|
|
|
+ rule.path.pop_back();
|
|
|
+ }
|
|
|
+ size_t i = 0;
|
|
|
+ while (i < rule.path.size() &&
|
|
|
+ (rule.path[i] == ' ' || rule.path[i] == '\t')) {
|
|
|
+ ++i;
|
|
|
+ }
|
|
|
+ rule.path = rule.path.substr(i);
|
|
|
+ if (rule.path.empty() || rule.path[0] != '/') {
|
|
|
+ err = "代理目录必须以 / 开头,例如 / 或 /web";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (unsigned char c : rule.path) {
|
|
|
+ if (c <= 0x20 || c == '"' || c == '\'' || c == ';') {
|
|
|
+ err = "代理目录含非法字符";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rule.target_type != "unix") {
|
|
|
+ rule.target_type = "url";
|
|
|
+ }
|
|
|
+ while (!rule.target.empty() &&
|
|
|
+ (rule.target.back() == ' ' || rule.target.back() == '\t')) {
|
|
|
+ rule.target.pop_back();
|
|
|
+ }
|
|
|
+ i = 0;
|
|
|
+ while (i < rule.target.size() &&
|
|
|
+ (rule.target[i] == ' ' || rule.target[i] == '\t')) {
|
|
|
+ ++i;
|
|
|
+ }
|
|
|
+ rule.target = rule.target.substr(i);
|
|
|
+ if (rule.target.empty()) {
|
|
|
+ err = "请填写目标地址";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (rule.target_type == "url") {
|
|
|
+ if (rule.target.rfind("http://", 0) != 0 &&
|
|
|
+ rule.target.rfind("https://", 0) != 0) {
|
|
|
+ rule.target = "http://" + rule.target;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (rule.target[0] != '/') {
|
|
|
+ err = "UNIX 目标应为 socket 文件路径,例如 /tmp/panel.sock";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rule.host.empty()) {
|
|
|
+ rule.host = "$http_host";
|
|
|
+ }
|
|
|
+ for (unsigned char c : rule.host) {
|
|
|
+ if (c <= 0x20 || c == '"' || c == ';' || c == '\n') {
|
|
|
+ err = "发送域名(host)含非法字符";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool apply_url_proxy_and_reload(const SiteInfo& s, std::string& err) {
|
|
|
+ if (!write_vhost(s)) {
|
|
|
+ err = "写入 Nginx 配置失败";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (vhost_enabled(s.name)) {
|
|
|
+ if (!apply_nginx()) {
|
|
|
+ err = "Nginx 重载失败,请检查配置";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
bool write_vhost(const SiteInfo& s) {
|
|
|
if (!ensure_dir(vhost_dir())) {
|
|
|
return false;
|
|
|
@@ -1043,9 +1372,11 @@ bool create_site_impl(const CreateSiteRequest& req, SiteInfo& created,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!start_site_by_name(s.name)) {
|
|
|
+ if (!start_site_by_name(s.name, err)) {
|
|
|
// Keep created site; caller can start later.
|
|
|
- log_warn("website created but start incomplete name=" + s.name);
|
|
|
+ log_warn("website created but start incomplete name=" + s.name +
|
|
|
+ (err.empty() ? "" : (" " + err)));
|
|
|
+ err.clear();
|
|
|
}
|
|
|
created = s;
|
|
|
created.running = site_is_running(s);
|
|
|
@@ -1108,6 +1439,7 @@ bool delete_site_impl(const std::string& name, bool delete_files,
|
|
|
stop_site_by_name(s.name);
|
|
|
remove_path(vhost_file(s));
|
|
|
remove_path(vhost_disabled_file(s));
|
|
|
+ remove_path(url_proxy_file(s));
|
|
|
|
|
|
auto remain = load_sites();
|
|
|
remain.erase(std::remove_if(remain.begin(), remain.end(),
|
|
|
@@ -1453,6 +1785,138 @@ bool update_site(const UpdateSiteRequest& req, std::string& err) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+std::vector<UrlProxyRule> list_url_proxies(const std::string& site_name,
|
|
|
+ std::string& err) {
|
|
|
+ err.clear();
|
|
|
+ SiteInfo s;
|
|
|
+ if (!find_site_info(site_name, s)) {
|
|
|
+ err = "未找到网站: " + site_name;
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ if (s.type != SiteType::Proxy && s.type != SiteType::Fastweb) {
|
|
|
+ err = "仅反向代理 / Fastweb 站点支持 URL 代理";
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ std::vector<UrlProxyRule> rules;
|
|
|
+ if (!load_url_proxy_rules(s, rules, err)) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ return rules;
|
|
|
+}
|
|
|
+
|
|
|
+bool add_url_proxy(const std::string& site_name, UrlProxyRule rule,
|
|
|
+ std::string& err) {
|
|
|
+ err.clear();
|
|
|
+ SiteInfo s;
|
|
|
+ if (!find_site_info(site_name, s)) {
|
|
|
+ err = "未找到网站: " + site_name;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (s.type != SiteType::Proxy && s.type != SiteType::Fastweb) {
|
|
|
+ err = "仅反向代理 / Fastweb 站点支持 URL 代理";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!normalize_url_proxy_rule(rule, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::vector<UrlProxyRule> rules;
|
|
|
+ if (!load_url_proxy_rules(s, rules, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (const auto& old : rules) {
|
|
|
+ if (old.path == rule.path) {
|
|
|
+ err = "代理目录已存在: " + rule.path;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (rule.id.empty()) {
|
|
|
+ rule.id = make_proxy_id();
|
|
|
+ }
|
|
|
+ rules.push_back(rule);
|
|
|
+ if (!save_url_proxy_rules(s, rules, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return apply_url_proxy_and_reload(s, err);
|
|
|
+}
|
|
|
+
|
|
|
+bool update_url_proxy(const std::string& site_name, const UrlProxyRule& in,
|
|
|
+ std::string& err) {
|
|
|
+ err.clear();
|
|
|
+ SiteInfo s;
|
|
|
+ if (!find_site_info(site_name, s)) {
|
|
|
+ err = "未找到网站: " + site_name;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (s.type != SiteType::Proxy && s.type != SiteType::Fastweb) {
|
|
|
+ err = "仅反向代理 / Fastweb 站点支持 URL 代理";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (in.id.empty()) {
|
|
|
+ err = "缺少代理规则 id";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ UrlProxyRule rule = in;
|
|
|
+ if (!normalize_url_proxy_rule(rule, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::vector<UrlProxyRule> rules;
|
|
|
+ if (!load_url_proxy_rules(s, rules, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ bool found = false;
|
|
|
+ for (auto& old : rules) {
|
|
|
+ if (old.id == rule.id) {
|
|
|
+ for (const auto& other : rules) {
|
|
|
+ if (other.id != rule.id && other.path == rule.path) {
|
|
|
+ err = "代理目录已存在: " + rule.path;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ old = rule;
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!found) {
|
|
|
+ err = "未找到代理规则";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!save_url_proxy_rules(s, rules, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return apply_url_proxy_and_reload(s, err);
|
|
|
+}
|
|
|
+
|
|
|
+bool delete_url_proxy(const std::string& site_name, const std::string& id,
|
|
|
+ std::string& err) {
|
|
|
+ err.clear();
|
|
|
+ SiteInfo s;
|
|
|
+ if (!find_site_info(site_name, s)) {
|
|
|
+ err = "未找到网站: " + site_name;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (id.empty()) {
|
|
|
+ err = "缺少代理规则 id";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::vector<UrlProxyRule> rules;
|
|
|
+ if (!load_url_proxy_rules(s, rules, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const auto before = rules.size();
|
|
|
+ rules.erase(std::remove_if(rules.begin(), rules.end(),
|
|
|
+ [&](const UrlProxyRule& r) { return r.id == id; }),
|
|
|
+ rules.end());
|
|
|
+ if (rules.size() == before) {
|
|
|
+ err = "未找到代理规则";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!save_url_proxy_rules(s, rules, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return apply_url_proxy_and_reload(s, err);
|
|
|
+}
|
|
|
+
|
|
|
bool apply_letsencrypt_impl(const std::string& name, const std::string& email,
|
|
|
const std::vector<std::string>& selected_hosts,
|
|
|
std::string& err) {
|
|
|
@@ -1612,8 +2076,10 @@ bool start_site(const std::string& name, std::string& err) {
|
|
|
err = "未找到网站: " + name;
|
|
|
return false;
|
|
|
}
|
|
|
- if (!start_site_by_name(name)) {
|
|
|
- err = "启动网站失败: " + name;
|
|
|
+ if (!start_site_by_name(name, err)) {
|
|
|
+ if (err.empty()) {
|
|
|
+ err = "启动网站失败: " + name;
|
|
|
+ }
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
@@ -1638,8 +2104,10 @@ bool restart_site(const std::string& name, std::string& err) {
|
|
|
err = "未找到网站: " + name;
|
|
|
return false;
|
|
|
}
|
|
|
- if (!restart_site_by_name(name)) {
|
|
|
- err = "重启网站失败: " + name;
|
|
|
+ if (!restart_site_by_name(name, err)) {
|
|
|
+ if (err.empty()) {
|
|
|
+ err = "重启网站失败: " + name;
|
|
|
+ }
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
@@ -1651,14 +2119,17 @@ bool delete_site(const std::string& name, std::string& err, bool delete_files) {
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
-bool start_fastweb_process(const SiteInfo& s) {
|
|
|
+bool start_fastweb_process(const SiteInfo& s, std::string& err) {
|
|
|
+ err.clear();
|
|
|
if (!fastweb::is_installed()) {
|
|
|
- std::cerr << "Fastweb 未安装。\n";
|
|
|
+ err = "Fastweb 未安装";
|
|
|
+ std::cerr << err << "。\n";
|
|
|
return false;
|
|
|
}
|
|
|
const std::string cfg = fastweb_config(s.name);
|
|
|
if (!path_exists(cfg)) {
|
|
|
- std::cerr << "未找到 Fastweb 配置: " << cfg << "\n";
|
|
|
+ err = "未找到 Fastweb 配置: " + cfg;
|
|
|
+ std::cerr << err << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
if (site_backend_running(s)) {
|
|
|
@@ -1680,7 +2151,8 @@ bool start_fastweb_process(const SiteInfo& s) {
|
|
|
|
|
|
const pid_t child = fork();
|
|
|
if (child < 0) {
|
|
|
- std::cerr << "启动 Fastweb 失败: fork 错误。\n";
|
|
|
+ err = "启动 Fastweb 失败: fork 错误";
|
|
|
+ std::cerr << err << "。\n";
|
|
|
return false;
|
|
|
}
|
|
|
if (child == 0) {
|
|
|
@@ -1722,10 +2194,16 @@ bool start_fastweb_process(const SiteInfo& s) {
|
|
|
_exit(127);
|
|
|
}
|
|
|
|
|
|
- bool ready = false;
|
|
|
+ auto reap_child = [child]() {
|
|
|
+ int st = 0;
|
|
|
+ waitpid(child, &st, WNOHANG);
|
|
|
+ };
|
|
|
+
|
|
|
+ bool saw_proc = false;
|
|
|
for (int i = 0; i < 50; ++i) {
|
|
|
+ reap_child();
|
|
|
if (!find_fastweb_pids(s.name).empty()) {
|
|
|
- ready = true;
|
|
|
+ saw_proc = true;
|
|
|
break;
|
|
|
}
|
|
|
if (!pid_alive(static_cast<int>(child))) {
|
|
|
@@ -1738,24 +2216,70 @@ bool start_fastweb_process(const SiteInfo& s) {
|
|
|
if (pos != std::string::npos) {
|
|
|
try {
|
|
|
const int port = std::stoi(s.upstream.substr(pos + 1));
|
|
|
- for (int i = 0; i < 50; ++i) {
|
|
|
- if (tcp_port_busy(port)) {
|
|
|
- ready = true;
|
|
|
+ for (int i = 0; i < 50 && !saw_proc; ++i) {
|
|
|
+ reap_child();
|
|
|
+ if (!find_fastweb_pids(s.name).empty()) {
|
|
|
+ saw_proc = true;
|
|
|
break;
|
|
|
}
|
|
|
+ // Port alone is not enough: another process may own it.
|
|
|
if (!pid_alive(static_cast<int>(child)) &&
|
|
|
find_fastweb_pids(s.name).empty()) {
|
|
|
break;
|
|
|
}
|
|
|
+ if (tcp_port_busy(port) && !find_fastweb_pids(s.name).empty()) {
|
|
|
+ saw_proc = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
usleep(100000);
|
|
|
}
|
|
|
+ (void)port;
|
|
|
} catch (...) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!ready && find_fastweb_pids(s.name).empty()) {
|
|
|
- std::cerr << "Fastweb 启动后未检测到运行,请查看站点日志目录: "
|
|
|
- << join_path(site_dir(s), "log") << "\n";
|
|
|
+ // Initialization scripts can crash shortly after the process appears.
|
|
|
+ if (saw_proc) {
|
|
|
+ for (int i = 0; i < 40; ++i) {
|
|
|
+ reap_child();
|
|
|
+ if (find_fastweb_pids(s.name).empty()) {
|
|
|
+ saw_proc = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ usleep(100000);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Give late starters a short extra window, then confirm still alive.
|
|
|
+ for (int i = 0; i < 20; ++i) {
|
|
|
+ reap_child();
|
|
|
+ if (!find_fastweb_pids(s.name).empty()) {
|
|
|
+ saw_proc = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ usleep(100000);
|
|
|
+ }
|
|
|
+ if (saw_proc) {
|
|
|
+ for (int i = 0; i < 20; ++i) {
|
|
|
+ reap_child();
|
|
|
+ if (find_fastweb_pids(s.name).empty()) {
|
|
|
+ saw_proc = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ usleep(100000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ reap_child();
|
|
|
+
|
|
|
+ if (!saw_proc || find_fastweb_pids(s.name).empty()) {
|
|
|
+ const std::string detail = fastweb_recent_error(s);
|
|
|
+ const std::string log_hint = join_path(site_dir(s), "log");
|
|
|
+ if (!detail.empty()) {
|
|
|
+ err = "Fastweb 启动失败: " + detail;
|
|
|
+ } else {
|
|
|
+ err = "Fastweb 启动后未保持运行,请查看日志: " + log_hint;
|
|
|
+ }
|
|
|
+ std::cerr << err << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@@ -1798,19 +2322,22 @@ bool stop_fastweb_process(const SiteInfo& s) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
-bool start_site_by_name(const std::string& name) {
|
|
|
+bool start_site_by_name(const std::string& name, std::string& err) {
|
|
|
+ err.clear();
|
|
|
SiteInfo s;
|
|
|
if (!find_site_info(name, s)) {
|
|
|
- std::cerr << "未找到网站: " << name << "\n";
|
|
|
+ err = "未找到网站: " + name;
|
|
|
+ std::cerr << err << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
if (!nginx::is_installed()) {
|
|
|
- std::cerr << "请先安装 Nginx。\n";
|
|
|
+ err = "请先安装 Nginx";
|
|
|
+ std::cerr << err << "。\n";
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
if (s.type == SiteType::Fastweb) {
|
|
|
- if (!start_fastweb_process(s)) {
|
|
|
+ if (!start_fastweb_process(s, err)) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
@@ -1819,7 +2346,8 @@ bool start_site_by_name(const std::string& name) {
|
|
|
if (!enable_vhost(s.name)) {
|
|
|
// disabled missing: regenerate
|
|
|
if (!write_vhost(s)) {
|
|
|
- std::cerr << "启用网站 vhost 失败。\n";
|
|
|
+ err = "启用网站 vhost 失败";
|
|
|
+ std::cerr << err << "。\n";
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
@@ -1830,9 +2358,21 @@ bool start_site_by_name(const std::string& name) {
|
|
|
|
|
|
if (!nginx::is_running()) {
|
|
|
if (!nginx::ensure_vhost_include() || !nginx::start()) {
|
|
|
+ err = "Nginx 启动失败";
|
|
|
return false;
|
|
|
}
|
|
|
} else if (!apply_nginx()) {
|
|
|
+ err = "Nginx 重载失败";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (s.type == SiteType::Fastweb && !site_backend_running(s)) {
|
|
|
+ const std::string detail = fastweb_recent_error(s);
|
|
|
+ err = detail.empty()
|
|
|
+ ? "网站 Nginx 已启用,但 Fastweb 未在运行"
|
|
|
+ : ("Fastweb 未在运行: " + detail);
|
|
|
+ std::cerr << "网站启动失败: " << s.name << " [" << site_status_label(s)
|
|
|
+ << "] " << err << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@@ -1867,10 +2407,12 @@ bool stop_site_by_name(const std::string& name) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
-bool restart_site_by_name(const std::string& name) {
|
|
|
+bool restart_site_by_name(const std::string& name, std::string& err) {
|
|
|
+ err.clear();
|
|
|
SiteInfo s;
|
|
|
if (!find_site_info(name, s)) {
|
|
|
- std::cerr << "未找到网站: " << name << "\n";
|
|
|
+ err = "未找到网站: " + name;
|
|
|
+ std::cerr << err << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@@ -1878,7 +2420,7 @@ bool restart_site_by_name(const std::string& name) {
|
|
|
// Stop backend/vhost first; ignore "already stopped" style failures for
|
|
|
// Fastweb-less static sites that are already disabled.
|
|
|
stop_site_by_name(name);
|
|
|
- if (!start_site_by_name(name)) {
|
|
|
+ if (!start_site_by_name(name, err)) {
|
|
|
std::cerr << "网站重启失败: " << name << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
@@ -1977,7 +2519,8 @@ void menu() {
|
|
|
case 4: {
|
|
|
std::string name;
|
|
|
if (pick_site_name(name)) {
|
|
|
- start_site_by_name(name);
|
|
|
+ std::string e;
|
|
|
+ start_site_by_name(name, e);
|
|
|
}
|
|
|
pause();
|
|
|
break;
|
|
|
@@ -1993,7 +2536,8 @@ void menu() {
|
|
|
case 6: {
|
|
|
std::string name;
|
|
|
if (pick_site_name(name)) {
|
|
|
- restart_site_by_name(name);
|
|
|
+ std::string e;
|
|
|
+ restart_site_by_name(name, e);
|
|
|
}
|
|
|
pause();
|
|
|
break;
|