#include "api_server.h" #include "../auth/auth.h" #include "../files/files.h" #include "../software/fastweb/fastweb.h" #include "../software/mysql/mysql.h" #include "../software/nginx/nginx.h" #include "../software/redis/redis.h" #include "../ssl/acme.h" #include "../store/store.h" #include "../system/metrics.h" #include "../system/metrics_history.h" #include "../system/process.h" #include "../tasks/tasks.h" #include "../utils.h" #include "../website/website.h" #include "../weblog/weblog.h" #include "net/http_center.h" #include "net/http_request.h" #include "net/http_response.h" #include "net/http_router.h" #include "net/http_subscribe.h" #include "net/http_website.h" #include "util/json.h" #include #include #include #include #include #include #include #include #include namespace ngs { namespace api { namespace { namespace fs = std::filesystem; using ylib::network::http::request; using ylib::network::http::response; using ylib::network::http::websocket_message; std::atomic g_running{true}; ylib::network::http::center* g_center = nullptr; void on_signal(int) { g_running.store(false, std::memory_order_relaxed); } void install_signal_handlers() { struct sigaction sa {}; sa.sa_handler = on_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; // interrupt blocking sleeps (do not set SA_RESTART) sigaction(SIGINT, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); } ylib::json parse_body(request* req) { if (!req) { return ylib::json(); } const std::string body = req->body().to_string(); if (body.empty()) { return ylib::json(); } try { return ylib::json::from(body); } catch (...) { return ylib::json(); } } std::string json_str(const ylib::json& j, const std::string& key, const std::string& def = "") { if (j.is_empty() || !j.exist(key)) { return def; } return j[key].to(true); } bool json_bool(const ylib::json& j, const std::string& key, bool def = false) { if (j.is_empty() || !j.exist(key)) { return def; } return j[key].to(true); } int json_int(const ylib::json& j, const std::string& key, int def = 0) { if (j.is_empty() || !j.exist(key)) { return def; } return j[key].to(true); } int hex_nibble(char c) { if (c >= '0' && c <= '9') { return c - '0'; } if (c >= 'a' && c <= 'f') { return c - 'a' + 10; } if (c >= 'A' && c <= 'F') { return c - 'A' + 10; } return -1; } // ylib get_url_param 不解码,需自行处理 %XX / + std::string url_decode(const std::string& in) { std::string out; out.reserve(in.size()); for (size_t i = 0; i < in.size(); ++i) { const char c = in[i]; if (c == '+') { out.push_back(' '); continue; } if (c == '%' && i + 2 < in.size()) { const int hi = hex_nibble(in[i + 1]); const int lo = hex_nibble(in[i + 2]); if (hi >= 0 && lo >= 0) { out.push_back(static_cast((hi << 4) | lo)); i += 2; continue; } } out.push_back(c); } return out; } std::string url_param(request* req, const std::string& key, const std::string& def = "") { if (!req) { return def; } std::string value; if (req->get_url_param(key, value) && !value.empty()) { return url_decode(value); } return def; } std::string b64_decode(const std::string& in) { static const int8_t kTable[256] = { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1}; std::string out; out.reserve(in.size() * 3 / 4); int val = 0, valb = -8; for (unsigned char c : in) { if (c == '=' || c == '\n' || c == '\r' || c == ' ') { continue; } const int8_t d = kTable[c]; if (d < 0) { continue; } val = (val << 6) + d; valb += 6; if (valb >= 0) { out.push_back(static_cast((val >> valb) & 0xFF)); valb -= 8; } } return out; } void reply_ok(response* resp, const ylib::json& data = ylib::json(), const std::string& msg = "ok") { ylib::json out; out["code"] = 200; out["msg"] = msg; out["data"] = data; resp->send(out); } void reply_err(response* resp, const std::string& msg, int code = -1) { ylib::json out; // ylib: operator=(int32) 会先转 uint64,负数会变成超大数;用 double 保留 -1 等错误码 out["code"] = static_cast(code); out["msg"] = msg; out["data"] = ylib::json(); resp->send(out); } bool require_method(request* req, response* resp, const std::string& method) { if (req->method() == method) { return true; } reply_err(resp, "method not allowed, expect " + method, 405); return false; } std::string request_cookie(request* req) { if (!req) { return ""; } std::string v; if (req->header("Cookie", v) || req->header("cookie", v)) { return v; } return ""; } std::string request_session_token(request* req) { return auth::session_from_cookie(request_cookie(req)); } bool request_authed(request* req, std::string* username = nullptr) { return auth::session_valid(request_session_token(req), username); } void set_session_cookie(response* resp, const std::string& token, bool clear) { if (!resp || !resp->headers()) { return; } if (clear || token.empty()) { (*resp->headers())["Set-Cookie"] = "ngs_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"; return; } (*resp->headers())["Set-Cookie"] = "ngs_session=" + token + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800"; } bool require_auth(request* req, response* resp) { if (request_authed(req)) { return true; } reply_err(resp, "未登录或会话已过期", 401); return false; } using Handler = void (*)(request*, response*); void reg(ylib::network::http::router* router, const std::string& path, Handler handler, bool auth_required = true) { router->subscribe()->add( path, "", [handler, auth_required](request* req, response* resp, websocket_message*, const std::string&, const std::string&) { if (auth_required && !require_auth(req, resp)) { return; } handler(req, resp); }); } // ---- handlers ---- void h_ping(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json data; data["name"] = "ngs"; data["listen"] = std::string(kDefaultListenAddr) + ":" + std::to_string(kDefaultListenPort); reply_ok(resp, data); } void h_auth_captcha(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const auth::CaptchaImage img = auth::create_captcha(); if (img.bmp.empty() || img.id.empty()) { reply_err(resp, "验证码生成失败", 500); return; } if (resp->headers()) { (*resp->headers())["Set-Cookie"] = "ngs_captcha=" + img.id + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=300"; (*resp->headers())["Content-Type"] = "image/bmp"; (*resp->headers())["Cache-Control"] = "no-store, no-cache, must-revalidate"; (*resp->headers())["Pragma"] = "no-cache"; } resp->send(img.bmp); } void h_auth_login(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const std::string username = json_str(body, "username"); const std::string password = json_str(body, "password"); const std::string captcha = json_str(body, "captcha"); if (username.empty() || password.empty()) { reply_err(resp, "请输入账号和密码", 400); return; } if (captcha.empty()) { reply_err(resp, "请输入验证码", 400); return; } const std::string captcha_id = auth::captcha_from_cookie(request_cookie(req)); if (!auth::consume_captcha(captcha_id, captcha)) { reply_err(resp, "验证码错误或已过期", 400); return; } if (!auth::verify(username, password)) { reply_err(resp, "账号或密码错误", 401); return; } const std::string token = auth::create_session(username); set_session_cookie(resp, token, false); ylib::json data; data["username"] = username; reply_ok(resp, data, "logged in"); } void h_auth_logout(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auth::destroy_session(request_session_token(req)); set_session_cookie(resp, "", true); reply_ok(resp, ylib::json(), "logged out"); } void h_auth_me(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } std::string username; if (!request_authed(req, &username)) { reply_err(resp, "未登录", 401); return; } ylib::json data; data["username"] = username; reply_ok(resp, data); } void reply_task_queued(response* resp, const std::string& task_id) { ylib::json data; data["task_id"] = task_id; data["active"] = tasks::active_count(); reply_ok(resp, data, "task queued"); } ylib::json task_to_json(const tasks::Task& t) { ylib::json item; item["id"] = t.id; item["type"] = t.type; item["title"] = t.title; item["status"] = tasks::status_str(t.status); item["created_at"] = static_cast(t.created_at); item["started_at"] = static_cast(t.started_at); item["finished_at"] = static_cast(t.finished_at); item["message"] = t.message; item["log_lines"] = static_cast(t.log_lines); return item; } void h_tasks_list(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json arr; for (const auto& t : tasks::list(false)) { arr.push_back(task_to_json(t)); } ylib::json data; data["active"] = tasks::active_count(); data["tasks"] = arr; reply_ok(resp, data); } void h_tasks_log(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const std::string id = url_param(req, "id"); size_t off = 0; try { off = static_cast(std::stoul(url_param(req, "offset", "0"))); } catch (...) { off = 0; } std::vector lines; size_t next = 0; size_t log_base = 0; bool truncated = false; tasks::Status st = tasks::Status::Pending; std::string message, err; if (!tasks::get_logs(id, off, lines, next, log_base, truncated, st, message, err)) { reply_err(resp, err); return; } ylib::json arr; for (const auto& line : lines) { arr.push_back(line); } ylib::json data; data["id"] = id; data["status"] = tasks::status_str(st); data["message"] = message; data["offset"] = static_cast(truncated ? log_base : off); data["next_offset"] = static_cast(next); data["log_base"] = static_cast(log_base); data["truncated"] = truncated; data["lines"] = arr; reply_ok(resp, data); } void h_tasks_clear(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } tasks::clear_finished(); ylib::json data; data["active"] = tasks::active_count(); reply_ok(resp, data, "cleared"); } void h_status(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json data; ylib::json nginx; nginx["installed"] = nginx::is_installed(); nginx["running"] = nginx::is_running(); nginx["version"] = nginx::installed_version(); nginx["conf"] = nginx::conf_file(); data["nginx"] = nginx; ylib::json fw; fw["installed"] = fastweb::is_installed(); fw["version"] = fastweb::installed_version(); data["fastweb"] = fw; ylib::json my; my["installed"] = mysql::is_installed(); my["running"] = mysql::is_running(); my["version"] = mysql::installed_version(); my["port"] = mysql::kDefaultPort; my["conf"] = mysql::conf_file(); data["mysql"] = my; ylib::json rd; rd["installed"] = redis::is_installed(); rd["running"] = redis::is_running(); rd["version"] = redis::installed_version(); rd["port"] = redis::listen_port(); rd["conf"] = redis::conf_file(); data["redis"] = rd; data["sites"] = static_cast(website::list_sites().size()); reply_ok(resp, data); } void h_nginx_status(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json data; data["installed"] = nginx::is_installed(); data["running"] = nginx::is_running(); data["version"] = nginx::installed_version(); data["install_dir"] = nginx::install_dir(); data["bin"] = nginx::bin_path(); data["conf"] = nginx::conf_file(); reply_ok(resp, data); } void h_nginx_install(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string version = json_str(body, "version", nginx::kDefaultVersion); std::string err; const std::string id = tasks::enqueue( "nginx.install", "安装 Nginx " + version, [version]() { return nginx::install(version); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_nginx_uninstall(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } std::string err; const std::string id = tasks::enqueue( "nginx.uninstall", "卸载 Nginx", []() { return nginx::uninstall(); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_nginx_start(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!nginx::start()) { reply_err(resp, "nginx start failed"); return; } reply_ok(resp, ylib::json(), "nginx started"); } void h_nginx_stop(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!nginx::stop()) { reply_err(resp, "nginx stop failed"); return; } reply_ok(resp, ylib::json(), "nginx stopped"); } void h_nginx_reload(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!nginx::reload()) { reply_err(resp, "nginx reload failed"); return; } reply_ok(resp, ylib::json(), "nginx reloaded"); } void h_fastweb_status(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json data; data["installed"] = fastweb::is_installed(); data["version"] = fastweb::installed_version(); data["install_dir"] = fastweb::install_dir(); data["bin"] = fastweb::bin_path(); reply_ok(resp, data); } void h_fastweb_install(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const bool debug = json_bool(body, "debug", false); std::string err; const std::string id = tasks::enqueue( "fastweb.install", "安装 Fastweb", [debug]() { return fastweb::install(debug); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_fastweb_uninstall(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } std::string err; const std::string id = tasks::enqueue( "fastweb.uninstall", "卸载 Fastweb", []() { return fastweb::uninstall(); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_fastweb_modules(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const std::string site = url_param(req, "site"); if (site.empty()) { reply_err(resp, "缺少 site 参数", 400); return; } std::vector list; std::string err; if (!fastweb::list_modules(site, list, err)) { reply_err(resp, err.empty() ? "获取模块列表失败" : err); return; } ylib::json arr; for (const auto& m : list) { ylib::json item; item["id"] = m.id; item["name"] = m.name; item["name_en"] = m.name_en; item["desc"] = m.desc; item["doc"] = m.doc; item["icon"] = m.icon; item["type"] = m.type; item["download_type"] = m.download_type; item["download_url"] = m.download_url; item["installed"] = m.installed; arr.push_back(item); } ylib::json data; data["site"] = site; data["catalog_url"] = fastweb::kModuleCatalogUrl; data["modules"] = arr; reply_ok(resp, data); } void h_fastweb_module_install(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const std::string site = json_str(body, "site"); const std::string name_en = json_str(body, "name_en"); if (site.empty() || name_en.empty()) { reply_err(resp, "请指定 site 与 name_en", 400); return; } std::string err; const std::string id = tasks::enqueue( "fastweb.module.install", "安装模块 " + name_en, [site, name_en]() { std::string e; return fastweb::install_module(site, name_en, e); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_fastweb_module_uninstall(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const std::string site = json_str(body, "site"); const std::string name_en = json_str(body, "name_en"); if (site.empty() || name_en.empty()) { reply_err(resp, "请指定 site 与 name_en", 400); return; } std::string err; const std::string id = tasks::enqueue( "fastweb.module.uninstall", "卸载模块 " + name_en, [site, name_en]() { std::string e; return fastweb::uninstall_module(site, name_en, e); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_mysql_status(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json data; data["installed"] = mysql::is_installed(); data["running"] = mysql::is_running(); data["version"] = mysql::installed_version(); data["install_dir"] = mysql::install_dir(); data["port"] = mysql::kDefaultPort; reply_ok(resp, data); } void h_mysql_install(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string version = json_str(body, "version", mysql::kDefaultVersion); std::string err; const std::string id = tasks::enqueue( "mysql.install", "安装 MySQL " + version, [version]() { return mysql::install(version); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_mysql_uninstall(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } std::string err; const std::string id = tasks::enqueue( "mysql.uninstall", "卸载 MySQL", []() { return mysql::uninstall(); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_mysql_start(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!mysql::start()) { reply_err(resp, "mysql start failed"); return; } reply_ok(resp, ylib::json(), "mysql started"); } void h_mysql_stop(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!mysql::stop()) { reply_err(resp, "mysql stop failed"); return; } reply_ok(resp, ylib::json(), "mysql stopped"); } void h_mysql_root_password(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string password = json_str(body, "password"); if (password.empty()) { reply_err(resp, "password required"); return; } if (!mysql::change_root_password(password)) { reply_err(resp, "change root password failed"); return; } reply_ok(resp, ylib::json(), "root password updated"); } void h_mysql_databases(request* req, response* resp) { const std::string method = req->method(); if (method == "GET") { ylib::json arr; for (const auto& db : mysql::list_databases()) { ylib::json item; item["name"] = db.name; item["user"] = db.user; item["password"] = db.password; item["access"] = db.access; item["host"] = db.host; arr.push_back(item); } reply_ok(resp, arr); return; } if (method == "POST") { auto body = parse_body(req); std::string name = json_str(body, "name"); std::string user = json_str(body, "user"); std::string password = json_str(body, "password"); if (name.empty() || user.empty() || password.empty()) { reply_err(resp, "name/user/password required"); return; } if (!mysql::create_database(name, user, password)) { reply_err(resp, "create database failed"); return; } reply_ok(resp, ylib::json(), "database created"); return; } reply_err(resp, "method not allowed", 405); } void h_mysql_databases_drop(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } if (!mysql::drop_database(name)) { reply_err(resp, "drop database failed"); return; } reply_ok(resp, ylib::json(), "database dropped"); } void h_mysql_databases_access(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string name = json_str(body, "name"); std::string mode = json_str(body, "mode"); std::string host = json_str(body, "host"); if (name.empty() || mode.empty()) { reply_err(resp, "name/mode required"); return; } if (!mysql::set_database_access(name, mode, host)) { reply_err(resp, "set database access failed"); return; } reply_ok(resp, ylib::json(), "database access updated"); } ylib::json site_to_json(const website::SiteInfo& s) { ylib::json item; item["name"] = s.name; item["type"] = website::type_key(s.type); const auto tokens = website::domain_tokens(s); item["domain"] = website::join_domains(tokens); ylib::json domains; for (const auto& t : tokens) { domains.push_back(t); } item["domains"] = domains; item["listen_port"] = s.listen_port; item["upstream"] = s.upstream; item["running"] = s.running; item["conf"] = website::vhost_conf_path(s.name); item["root"] = website::site_root_path(s.name); const std::string log_dir = website::site_log_dir_path(s.name); item["log_dir"] = log_dir; item["access_log"] = join_path(log_dir, "access.log"); item["error_log"] = join_path(log_dir, "error.log"); item["log_db"] = weblog::db_path(website::site_key(s)); item["ssl_enable"] = s.ssl_enable; item["ssl_cert"] = s.ssl_cert; item["ssl_key"] = s.ssl_key; item["ssl_port"] = s.ssl_port; item["ssl_not_before"] = ""; item["ssl_not_after"] = ""; item["ssl_days_left"] = -1; if (!s.ssl_cert.empty()) { const ssl::CertInfo ci = ssl::read_cert_info(s.ssl_cert); if (ci.ok) { item["ssl_not_before"] = ci.not_before; item["ssl_not_after"] = ci.not_after; item["ssl_days_left"] = ci.days_left; } } return item; } void h_websites(request* req, response* resp) { const std::string method = req->method(); if (method == "GET") { ylib::json arr; for (const auto& s : website::list_sites()) { arr.push_back(site_to_json(s)); } reply_ok(resp, arr); return; } if (method == "POST") { auto body = parse_body(req); website::CreateSiteRequest creq; creq.name = json_str(body, "name"); std::string type = json_str(body, "type", "static"); if (!website::parse_type_key(type, creq.type)) { reply_err(resp, "invalid type, expect static|fastweb"); return; } // "proxy" is accepted as alias of static for older clients. if (type == "proxy") { creq.type = website::SiteType::Static; } creq.domain = json_str(body, "domain"); if (body.exist("domains") && body["domains"].is_array()) { std::string joined; const auto& arr = body["domains"]; for (uint32 i = 0; i < arr.size(); ++i) { std::string d = arr[i].to(true); if (d.empty()) { continue; } if (!joined.empty()) { joined.push_back(' '); } joined += d; } if (!joined.empty()) { creq.domain = joined; } } creq.listen_port = json_int(body, "listen_port", 0); creq.upstream.clear(); creq.root = json_str(body, "root"); if (creq.name.empty()) { reply_err(resp, "name required"); return; } if (creq.domain.empty()) { reply_err(resp, "domain required"); return; } std::string err; if (!website::create_site(creq, err)) { reply_err(resp, err.empty() ? "create site failed" : err); return; } website::SiteInfo created; website::find_site(creq.name, created); reply_ok(resp, site_to_json(created), "site created"); return; } reply_err(resp, "method not allowed", 405); } void h_websites_start(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } std::string err; if (!website::start_site(name, err)) { reply_err(resp, err.empty() ? "start site failed" : err); return; } reply_ok(resp, ylib::json(), "site started"); } void h_websites_stop(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } std::string err; if (!website::stop_site(name, err)) { reply_err(resp, err.empty() ? "stop site failed" : err); return; } reply_ok(resp, ylib::json(), "site stopped"); } void h_websites_restart(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } std::string err; if (!website::restart_site(name, err)) { reply_err(resp, err.empty() ? "restart site failed" : err); return; } reply_ok(resp, ylib::json(), "site restarted"); } void h_websites_delete(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } const bool delete_files = json_bool(body, "delete_files", false); std::string err; if (!website::delete_site(name, err, delete_files)) { reply_err(resp, err.empty() ? "delete site failed" : err); return; } reply_ok(resp, ylib::json(), delete_files ? "site and files deleted" : "site deleted"); } void h_websites_update(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); website::UpdateSiteRequest ureq; ureq.name = json_str(body, "name"); if (ureq.name.empty()) { reply_err(resp, "name required"); return; } if (body.exist("domains") && body["domains"].is_array()) { std::string joined; const auto& arr = body["domains"]; for (uint32 i = 0; i < arr.size(); ++i) { std::string d = arr[i].to(true); if (d.empty()) { continue; } if (!joined.empty()) { joined.push_back(' '); } joined += d; } ureq.domain = joined; } else { ureq.domain = json_str(body, "domain"); } ureq.listen_port = json_int(body, "listen_port", 0); ureq.update_ssl = body.exist("ssl_enable") || body.exist("ssl_cert") || body.exist("ssl_key") || body.exist("ssl_port"); ureq.ssl_enable = json_bool(body, "ssl_enable", false); ureq.ssl_cert = json_str(body, "ssl_cert"); ureq.ssl_key = json_str(body, "ssl_key"); ureq.ssl_port = json_int(body, "ssl_port", 443); std::string err; if (!website::update_site(ureq, err)) { reply_err(resp, err.empty() ? "update site failed" : err); return; } website::SiteInfo updated; website::find_site(ureq.name, updated); reply_ok(resp, site_to_json(updated), "site updated"); } ylib::json url_proxy_to_json(const website::UrlProxyRule& r) { 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; return item; } void h_websites_proxies(request* req, response* resp) { const std::string method = req ? req->method() : ""; if (method == "GET") { const std::string name = url_param(req, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } std::string err; auto rules = website::list_url_proxies(name, err); if (!err.empty()) { reply_err(resp, err); return; } ylib::json arr; for (const auto& r : rules) { arr.push_back(url_proxy_to_json(r)); } ylib::json data; data["proxies"] = arr; reply_ok(resp, data); return; } if (method == "POST") { auto body = parse_body(req); const std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } website::UrlProxyRule rule; rule.path = json_str(body, "path"); rule.target_type = json_str(body, "target_type", "url"); rule.target = json_str(body, "target"); rule.host = json_str(body, "host", "$http_host"); rule.remark = json_str(body, "remark"); std::string err; if (!website::add_url_proxy(name, rule, err)) { reply_err(resp, err.empty() ? "add url proxy failed" : err); return; } auto rules = website::list_url_proxies(name, err); ylib::json arr; for (const auto& r : rules) { arr.push_back(url_proxy_to_json(r)); } ylib::json data; data["proxies"] = arr; reply_ok(resp, data, "url proxy added"); return; } reply_err(resp, "method not allowed", 405); } void h_websites_proxies_update(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const std::string name = json_str(body, "name"); website::UrlProxyRule rule; rule.id = json_str(body, "id"); rule.path = json_str(body, "path"); rule.target_type = json_str(body, "target_type", "url"); rule.target = json_str(body, "target"); rule.host = json_str(body, "host", "$http_host"); rule.remark = json_str(body, "remark"); if (name.empty() || rule.id.empty()) { reply_err(resp, "name and id required"); return; } std::string err; if (!website::update_url_proxy(name, rule, err)) { reply_err(resp, err.empty() ? "update url proxy failed" : err); return; } reply_ok(resp, ylib::json(), "url proxy updated"); } void h_websites_proxies_delete(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const std::string name = json_str(body, "name"); const std::string id = json_str(body, "id"); if (name.empty() || id.empty()) { reply_err(resp, "name and id required"); return; } std::string err; if (!website::delete_url_proxy(name, id, err)) { reply_err(resp, err.empty() ? "delete url proxy failed" : err); return; } reply_ok(resp, ylib::json(), "url proxy deleted"); } void h_websites_logs(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const std::string name = url_param(req, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } website::SiteInfo site; if (!website::find_site(name, site)) { reply_err(resp, "site not found", 404); return; } const std::string kind = url_param(req, "kind", "access"); size_t limit = 100; try { limit = static_cast(std::stoul(url_param(req, "limit", "100"))); } catch (...) { limit = 100; } int64_t before_id = 0; try { before_id = std::stoll(url_param(req, "before_id", "0")); } catch (...) { before_id = 0; } ylib::json arr; std::string err; if (kind == "error") { weblog::ErrorQuery q; q.name = name; q.limit = limit; q.before_id = before_id; q.level = url_param(req, "level"); std::vector rows; if (!weblog::query_error(q, rows, err)) { reply_err(resp, err.empty() ? "query logs failed" : err); return; } for (const auto& r : rows) { ylib::json item; item["id"] = static_cast(r.id); item["time"] = r.time; item["time_ms"] = static_cast(r.time_ms); item["level"] = r.level; item["message"] = r.message; item["raw"] = r.raw; arr.push_back(item); } } else { weblog::AccessQuery q; q.name = name; q.limit = limit; q.before_id = before_id; try { q.status = std::stoi(url_param(req, "status", "0")); } catch (...) { q.status = 0; } q.ip = url_param(req, "ip"); std::vector rows; if (!weblog::query_access(q, rows, err)) { reply_err(resp, err.empty() ? "query logs failed" : err); return; } for (const auto& r : rows) { ylib::json item; item["id"] = static_cast(r.id); item["time"] = r.time; item["time_ms"] = static_cast(r.time_ms); item["ip"] = r.ip; item["method"] = r.method; item["host"] = r.host; item["uri"] = r.uri; item["args"] = r.args; item["status"] = r.status; item["bytes_recv"] = static_cast(r.bytes_recv); item["bytes_sent"] = static_cast(r.bytes_sent); item["request_time"] = r.request_time; item["upstream_status"] = r.upstream_status; item["upstream_addr"] = r.upstream_addr; item["referer"] = r.referer; item["ua"] = r.ua; item["request"] = r.request; arr.push_back(item); } } ylib::json data; data["name"] = name; data["kind"] = kind == "error" ? "error" : "access"; data["log_db"] = weblog::db_path(website::site_key_by_name(name)); data["items"] = arr; reply_ok(resp, data); } void h_websites_analytics(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const std::string name = url_param(req, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } website::SiteInfo site; if (!website::find_site(name, site)) { reply_err(resp, "site not found", 404); return; } std::string range = url_param(req, "range", "24h"); if (range.empty()) { range = "24h"; } std::string json_body; std::string err; if (!weblog::analyze(name, range, json_body, err)) { reply_err(resp, err.empty() ? "analyze failed" : err); return; } ylib::json data; try { data = ylib::json::from(json_body); } catch (...) { reply_err(resp, "invalid analytics payload"); return; } reply_ok(resp, data); } void h_websites_ssl_apply(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const std::string name = json_str(body, "name"); if (name.empty()) { reply_err(resp, "name required"); return; } std::vector hosts; if (body.exist("domains") && body["domains"].is_array()) { const auto& arr = body["domains"]; for (uint32 i = 0; i < arr.size(); ++i) { std::string d = arr[i].to(true); if (!d.empty()) { hosts.push_back(d); } } } else if (body.exist("hosts") && body["hosts"].is_array()) { const auto& arr = body["hosts"]; for (uint32 i = 0; i < arr.size(); ++i) { std::string d = arr[i].to(true); if (!d.empty()) { hosts.push_back(d); } } } // ACME account contact: md5()@qq.com (LE requires public-suffix email). // Ignore previously stored invalid/legacy emails. std::string email = json_str(body, "email"); if (!ssl::is_valid_acme_email(email) || email.size() < 8 || email.compare(email.size() - 7, 7, "@qq.com") != 0) { std::string domain_hint; if (!hosts.empty()) { domain_hint = hosts.front(); } else { website::SiteInfo site; if (website::find_site(name, site)) { domain_hint = website::primary_domain(site.domain); } } email = ssl::default_account_email(domain_hint); if (!ssl::is_valid_acme_email(email)) { reply_err(resp, "无法生成 ACME 联系邮箱"); return; } store::set_setting("ssl_email", email); } else if (store::get_setting("ssl_email", "") != email) { store::set_setting("ssl_email", email); } std::string err; std::string title = "申请 SSL · " + name; if (!hosts.empty()) { title += " (" + std::to_string(hosts.size()) + ")"; } const std::string id = tasks::enqueue( "ssl.letsencrypt", title, [name, email, hosts]() { std::string e; if (!website::apply_letsencrypt(name, email, hosts, e)) { if (!e.empty()) { std::cerr << e << "\n"; log_error(e); } return false; } return true; }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } ylib::json data; data["task_id"] = id; data["active"] = tasks::active_count(); reply_ok(resp, data, "task queued"); } void h_system_metrics(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const auto m = system::sample(); ylib::json data; ylib::json cpu; cpu["percent"] = m.cpu.percent; cpu["cores"] = m.cpu.cores; data["cpu"] = cpu; ylib::json memory; memory["total"] = static_cast(m.memory.total_bytes); memory["used"] = static_cast(m.memory.used_bytes); memory["percent"] = m.memory.percent; data["memory"] = memory; ylib::json disk; disk["path"] = m.disk.path; disk["total"] = static_cast(m.disk.total_bytes); disk["used"] = static_cast(m.disk.used_bytes); disk["percent"] = m.disk.percent; data["disk"] = disk; ylib::json network; network["rx_bytes"] = static_cast(m.network.rx_bytes); network["tx_bytes"] = static_cast(m.network.tx_bytes); network["rx_bps"] = m.network.rx_bps; network["tx_bps"] = m.network.tx_bps; data["network"] = network; reply_ok(resp, data); } void h_system_processes(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } const std::string q = url_param(req, "q", ""); const auto list = system::list_processes(q); ylib::json arr; for (const auto& p : list) { ylib::json item; item["pid"] = p.pid; item["ppid"] = p.ppid; item["user"] = p.user; item["state"] = p.state; item["name"] = p.name; item["cmdline"] = p.cmdline; item["threads"] = p.threads; item["rss"] = static_cast(p.rss_bytes); item["vms"] = static_cast(p.vms_bytes); item["mem_percent"] = p.mem_percent; item["cpu_percent"] = p.cpu_percent; arr.push_back(item); } ylib::json data; data["total"] = static_cast(list.size()); data["items"] = arr; reply_ok(resp, data); } void h_system_processes_kill(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); const int pid = json_int(body, "pid", 0); int signal = json_int(body, "signal", 15); const std::string sig_name = json_str(body, "signal_name"); if (!sig_name.empty()) { if (sig_name == "KILL" || sig_name == "SIGKILL" || sig_name == "9") { signal = 9; } else if (sig_name == "TERM" || sig_name == "SIGTERM" || sig_name == "15") { signal = 15; } else if (sig_name == "INT" || sig_name == "SIGINT" || sig_name == "2") { signal = 2; } else if (sig_name == "HUP" || sig_name == "SIGHUP" || sig_name == "1") { signal = 1; } } if (pid <= 0) { reply_err(resp, "pid required"); return; } std::string err; if (!system::kill_process(pid, signal, err)) { reply_err(resp, err.empty() ? "kill failed" : err); return; } ylib::json data; data["pid"] = pid; data["signal"] = signal; reply_ok(resp, data, "process signaled"); } void h_system_metrics_history(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } std::string range = url_param(req, "range", "10m"); if (range.empty()) { range = "10m"; } std::vector points; std::string err; if (!system::history_query(range, points, err)) { reply_err(resp, err.empty() ? "query metrics history failed" : err); return; } ylib::json arr; for (const auto& p : points) { ylib::json item; item["time_ms"] = static_cast(p.time_ms); item["cpu_percent"] = p.cpu_percent; item["cpu_cores"] = p.cpu_cores; item["mem_percent"] = p.mem_percent; item["mem_used"] = static_cast(p.mem_used); item["mem_total"] = static_cast(p.mem_total); item["disk_percent"] = p.disk_percent; item["disk_used"] = static_cast(p.disk_used); item["disk_total"] = static_cast(p.disk_total); item["net_rx_bps"] = p.net_rx_bps; item["net_tx_bps"] = p.net_tx_bps; arr.push_back(item); } ylib::json data; data["range"] = range; data["items"] = arr; reply_ok(resp, data); } void h_redis_status(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } ylib::json data; data["installed"] = redis::is_installed(); data["running"] = redis::is_running(); data["version"] = redis::installed_version(); data["install_dir"] = redis::install_dir(); data["bin"] = redis::bin_path(); data["conf"] = redis::conf_file(); data["port"] = redis::listen_port(); reply_ok(resp, data); } void h_redis_install(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string version = json_str(body, "version", redis::kDefaultVersion); std::string err; const std::string id = tasks::enqueue( "redis.install", "安装 Redis " + version, [version]() { return redis::install(version); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_redis_uninstall(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } std::string err; const std::string id = tasks::enqueue( "redis.uninstall", "卸载 Redis", []() { return redis::uninstall(); }, err); if (id.empty()) { reply_err(resp, err.empty() ? "enqueue failed" : err); return; } reply_task_queued(resp, id); } void h_redis_start(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!redis::start()) { reply_err(resp, "redis start failed"); return; } reply_ok(resp, ylib::json(), "redis started"); } void h_redis_stop(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } if (!redis::stop()) { reply_err(resp, "redis stop failed"); return; } reply_ok(resp, ylib::json(), "redis stopped"); } void h_files_list(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } std::string path = url_param(req, "path", "/"); std::string abs, err; if (!files::resolve_path(path, abs, err)) { reply_err(resp, err); return; } std::vector entries; if (!files::list_dir(abs, entries, err)) { reply_err(resp, err); return; } ylib::json arr; for (const auto& e : entries) { ylib::json item; item["name"] = e.name; item["path"] = e.path; item["is_dir"] = e.is_dir; item["is_symlink"] = e.is_symlink; item["size"] = static_cast(e.size); item["mtime"] = static_cast(e.mtime); arr.push_back(item); } ylib::json data; data["path"] = abs; data["entries"] = arr; reply_ok(resp, data); } void h_files_mkdir(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string path = json_str(body, "path"); std::string err; if (!files::mkdir_path(path, err)) { reply_err(resp, err); return; } reply_ok(resp, ylib::json(), "directory created"); } void h_files_write(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string path = json_str(body, "path"); std::string content = json_str(body, "content"); std::string err; if (!files::write_text(path, content, err)) { reply_err(resp, err); return; } reply_ok(resp, ylib::json(), "written"); } void h_files_read(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } std::string path = url_param(req, "path"); std::string content, err; if (!files::read_text(path, content, err)) { reply_err(resp, err); return; } ylib::json data; data["path"] = path; data["content"] = content; reply_ok(resp, data); } void h_files_tail(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } std::string path = url_param(req, "path"); size_t max_bytes = 256ull * 1024ull; try { max_bytes = static_cast( std::stoul(url_param(req, "bytes", "262144"))); } catch (...) { } if (max_bytes < 1024) { max_bytes = 1024; } if (max_bytes > files::kMaxEditBytes) { max_bytes = files::kMaxEditBytes; } std::string content, err; if (!files::read_tail(path, max_bytes, content, err)) { reply_err(resp, err); return; } ylib::json data; data["path"] = path; data["content"] = content; reply_ok(resp, data); } void h_files_rename(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string from = json_str(body, "from"); std::string to = json_str(body, "to"); std::string err; if (!files::rename_path(from, to, err)) { reply_err(resp, err); return; } reply_ok(resp, ylib::json(), "renamed"); } void h_files_delete(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string path = json_str(body, "path"); const bool recursive = json_bool(body, "recursive", false); std::string err; if (!files::delete_path(path, recursive, err)) { reply_err(resp, err); return; } reply_ok(resp, ylib::json(), "deleted"); } void h_files_upload(request* req, response* resp) { if (!require_method(req, resp, "POST")) { return; } auto body = parse_body(req); std::string path = json_str(body, "path"); std::string b64 = json_str(body, "content_base64"); std::string content = json_str(body, "content"); std::string bytes = b64.empty() ? content : b64_decode(b64); std::string err; if (!files::write_bytes(path, bytes, err)) { reply_err(resp, err); return; } reply_ok(resp, ylib::json(), "uploaded"); } void h_files_download(request* req, response* resp) { if (!require_method(req, resp, "GET")) { return; } std::string path = url_param(req, "path"); std::string abs, err; if (!files::resolve_path(path, abs, err)) { reply_err(resp, err); return; } if (!path_exists(abs) || is_dir(abs)) { reply_err(resp, "file not found"); return; } std::string name = fs::path(abs).filename().string(); if (name.empty()) { name = "download"; } // Keep original filename for browsers (ASCII fallback + RFC5987 UTF-8). std::string safe; for (unsigned char c : name) { if (c == '"' || c == '\\' || c < 0x20) { safe.push_back('_'); } else { safe.push_back(static_cast(c)); } } std::string encoded; static const char* hex = "0123456789ABCDEF"; for (unsigned char c : name) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') { encoded.push_back(static_cast(c)); } else { encoded.push_back('%'); encoded.push_back(hex[c >> 4]); encoded.push_back(hex[c & 0xF]); } } (*resp->headers())["Content-Disposition"] = "attachment; filename=\"" + safe + "\"; filename*=UTF-8''" + encoded; (*resp->headers())["Content-Type"] = "application/octet-stream"; resp->send_file(abs); } void register_routes(ylib::network::http::router* router) { // Public endpoints reg(router, "/api/ping", h_ping, false); reg(router, "/api/auth/captcha", h_auth_captcha, false); reg(router, "/api/auth/login", h_auth_login, false); reg(router, "/api/auth/logout", h_auth_logout, false); reg(router, "/api/auth/me", h_auth_me, false); reg(router, "/api/status", h_status); reg(router, "/api/system/metrics", h_system_metrics); reg(router, "/api/system/metrics/history", h_system_metrics_history); reg(router, "/api/system/processes", h_system_processes); reg(router, "/api/system/processes/kill", h_system_processes_kill); reg(router, "/api/tasks", h_tasks_list); reg(router, "/api/tasks/log", h_tasks_log); reg(router, "/api/tasks/clear", h_tasks_clear); reg(router, "/api/nginx/status", h_nginx_status); reg(router, "/api/nginx/install", h_nginx_install); reg(router, "/api/nginx/uninstall", h_nginx_uninstall); reg(router, "/api/nginx/start", h_nginx_start); reg(router, "/api/nginx/stop", h_nginx_stop); reg(router, "/api/nginx/reload", h_nginx_reload); reg(router, "/api/fastweb/status", h_fastweb_status); reg(router, "/api/fastweb/install", h_fastweb_install); reg(router, "/api/fastweb/uninstall", h_fastweb_uninstall); reg(router, "/api/fastweb/modules", h_fastweb_modules); reg(router, "/api/fastweb/modules/install", h_fastweb_module_install); reg(router, "/api/fastweb/modules/uninstall", h_fastweb_module_uninstall); reg(router, "/api/mysql/status", h_mysql_status); reg(router, "/api/mysql/install", h_mysql_install); reg(router, "/api/mysql/uninstall", h_mysql_uninstall); reg(router, "/api/mysql/start", h_mysql_start); reg(router, "/api/mysql/stop", h_mysql_stop); reg(router, "/api/mysql/root/password", h_mysql_root_password); reg(router, "/api/mysql/databases", h_mysql_databases); reg(router, "/api/mysql/databases/drop", h_mysql_databases_drop); reg(router, "/api/mysql/databases/access", h_mysql_databases_access); reg(router, "/api/redis/status", h_redis_status); reg(router, "/api/redis/install", h_redis_install); reg(router, "/api/redis/uninstall", h_redis_uninstall); reg(router, "/api/redis/start", h_redis_start); reg(router, "/api/redis/stop", h_redis_stop); reg(router, "/api/websites", h_websites); reg(router, "/api/websites/start", h_websites_start); reg(router, "/api/websites/stop", h_websites_stop); reg(router, "/api/websites/restart", h_websites_restart); reg(router, "/api/websites/delete", h_websites_delete); reg(router, "/api/websites/update", h_websites_update); reg(router, "/api/websites/proxies", h_websites_proxies); reg(router, "/api/websites/proxies/update", h_websites_proxies_update); reg(router, "/api/websites/proxies/delete", h_websites_proxies_delete); reg(router, "/api/websites/logs", h_websites_logs); reg(router, "/api/websites/analytics", h_websites_analytics); reg(router, "/api/websites/ssl/apply", h_websites_ssl_apply); reg(router, "/api/files/list", h_files_list); reg(router, "/api/files/mkdir", h_files_mkdir); reg(router, "/api/files/write", h_files_write); reg(router, "/api/files/read", h_files_read); reg(router, "/api/files/tail", h_files_tail); reg(router, "/api/files/rename", h_files_rename); reg(router, "/api/files/delete", h_files_delete); reg(router, "/api/files/upload", h_files_upload); reg(router, "/api/files/download", h_files_download); const std::string www = panel_www_dir(); router->other([www](request* req, response* resp, websocket_message*) { std::string path = req ? req->filepath() : "/"; if (path.empty() || path == "/") { path = "/index.html"; } if (path.rfind("/api/", 0) == 0) { reply_err(resp, "not found", 404); return; } if (path.find("..") != std::string::npos) { reply_err(resp, "forbidden", 403); return; } const bool is_asset = path.rfind("/assets/", 0) == 0 || path == "/favicon.ico"; const bool is_login = path == "/login.html" || path == "/login" || path == "login.html"; if (!is_asset && !is_login && !request_authed(req)) { resp->redirect("/login.html", false); return; } if (is_login && request_authed(req)) { resp->redirect("/", false); return; } while (!path.empty() && path[0] == '/') { path.erase(path.begin()); } if (path == "login") { path = "login.html"; } std::string file = join_path(www, path); if (is_dir(file)) { file = join_path(file, "index.html"); } if (path_exists(file) && !is_dir(file)) { resp->send_file(file); return; } const std::string index = join_path(www, "index.html"); if (path_exists(index)) { resp->send_file(index); return; } reply_err(resp, "panel not found: " + www, 404); }); } } // namespace bool run(const std::string& listen_addr, uint16_t listen_port) { (void)listen_addr; // ylib HttpServer always binds 0.0.0.0 ensure_dir(software_root()); ensure_dir(conf_root()); ensure_dir(join_path(conf_root(), "ssl")); ensure_dir(wwwroot_dir()); { std::string aerr; if (!auth::init(aerr)) { std::cerr << "apiserver auth init failed: " << aerr << "\n"; log_error("auth init failed: " + aerr); return false; } } if (!store::init()) { std::cerr << "apiserver sqlite init failed: " << store::database_path() << "\n"; log_error("sqlite init failed path=" + store::database_path()); return false; } tasks::start(); weblog::start(); system::history_start(); log_info("ngs apiserver starting listen=" + listen_addr + ":" + std::to_string(listen_port)); install_signal_handlers(); auto* center = new ylib::network::http::center(); g_center = center; ylib::network::http::start_config config; ylib::network::http::website_config ws_config; ws_config.name = "ngs-api"; // domain containing 0.0.0.0 matches any Host header in ylib. ylib::network::http::host_config host; host.domain = listen_addr.empty() ? kDefaultListenAddr : listen_addr; host.port = listen_port; host.ssl = false; ws_config.host.push_back(host); ws_config.router.threadpool.size = 8; ws_config.router.threadpool.queuemax = 10000; config.max_upload_size = 64 * 1024 * 1024; config.websocket_enable = false; config.website.push_back(ws_config); if (!center->create(config)) { std::cerr << "apiserver create failed: " << center->last_error() << "\n"; log_error("apiserver create failed: " + center->last_error()); delete center; g_center = nullptr; return false; } auto* website = center->website_byname(ws_config.name); if (!website || !website->router()) { std::cerr << "apiserver website/router missing\n"; log_error("apiserver website/router missing"); center->close(); delete center; g_center = nullptr; return false; } register_routes(website->router()); if (!center->start()) { std::cerr << "apiserver start failed: " << center->last_error() << "\n"; log_error("apiserver start failed: " + center->last_error()); center->close(); delete center; g_center = nullptr; return false; } // HPSocket may touch signal state during start; reinstall our handlers. install_signal_handlers(); std::cout << "NGS API Server listening on http://" << host.domain << ":" << listen_port << "\n"; std::cout << "Panel: http://127.0.0.1:" << listen_port << "/\n"; std::cout << "Panel dir: " << panel_www_dir() << "\n"; std::cout << "Docs: API.md\n"; std::cout.flush(); log_info("ngs apiserver listening port=" + std::to_string(listen_port) + " panel=" + panel_www_dir()); while (g_running.load(std::memory_order_relaxed)) { std::this_thread::sleep_for(std::chrono::milliseconds(200)); } std::cout << "Shutting down...\n"; log_info("ngs apiserver stopping"); weblog::stop(); system::history_stop(); tasks::stop(); center->close(); delete center; g_center = nullptr; return true; } } // namespace api } // namespace ngs