From 806b1d75afd27a808ac60fae43eeda1f4efb43dd Mon Sep 17 00:00:00 2001 From: aiamnezia Date: Wed, 31 Dec 2025 19:30:29 +0400 Subject: [PATCH] refactor: streamline HTTP API for Xray control - Removed outdated config management routes and consolidated Xray control endpoints. - Updated response structures to ensure consistent error handling across API calls. --- client/core/local-proxy/httpapi.cpp | 587 +++++----------------------- client/core/local-proxy/httpapi.h | 15 +- 2 files changed, 101 insertions(+), 501 deletions(-) diff --git a/client/core/local-proxy/httpapi.cpp b/client/core/local-proxy/httpapi.cpp index 9899ced5a..3c9534966 100644 --- a/client/core/local-proxy/httpapi.cpp +++ b/client/core/local-proxy/httpapi.cpp @@ -2,11 +2,56 @@ #include "proxylogger.h" #include #include -#include -#include #include -#include -#include +#include + +namespace { + +std::optional extractInboundPort(const QJsonObject &config) +{ + if (!config.contains("inbounds") || !config["inbounds"].isArray()) { + return std::nullopt; + } + + const QJsonArray inbounds = config["inbounds"].toArray(); + if (inbounds.isEmpty() || !inbounds.at(0).isObject()) { + return std::nullopt; + } + + const QJsonObject firstInbound = inbounds.at(0).toObject(); + if (!firstInbound.contains("port")) { + return std::nullopt; + } + + return firstInbound.value("port").toInt(); +} + +QJsonValue proxyPortValue(const std::optional &port) +{ + if (port.has_value()) { + return QJsonValue(*port); + } + return QJsonValue::Null; +} + +QHttpServerResponse makeServiceUnavailablePingResponse() +{ + QJsonObject payload{ + {"status", "error"}, + {"proxyPort", QJsonValue::Null} + }; + return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable); +} + +QHttpServerResponse makeServiceUnavailableStatusResponse() +{ + QJsonObject payload{ + {"status", "error"} + }; + return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable); +} + +} // namespace HttpApi::HttpApi(QWeakPointer service, QObject* parent) : QObject(parent) @@ -35,12 +80,6 @@ bool HttpApi::start(quint16 port) ProxyLogger::getInstance().info(QString("HTTP API server is running on localhost:%1").arg(m_tcpServer->serverPort())); ProxyLogger::getInstance().debug("Available endpoints:\n" - " GET /api/v1/configs\n" - " POST /api/v1/configs\n" - " PUT /api/v1/configs\n" - " DELETE /api/v1/configs\n" - " PUT /api/v1/configs/activate\n" - " GET /api/v1/configs/active\n" " POST /api/v1/up\n" " POST /api/v1/down\n" " GET /api/v1/ping"); @@ -59,523 +98,93 @@ void HttpApi::setupRoutes() { ProxyLogger::getInstance().debug("Setting up HTTP API routes"); - // Config management routes - m_server.route("/api/v1/configs", QHttpServerRequest::Method::Get, - [this](const QHttpServerRequest &request) { - ProxyLogger::getInstance().debug("Handling GET /api/v1/configs request"); - return handleGetConfigs(request); - }); - - m_server.route("/api/v1/configs", QHttpServerRequest::Method::Post, - [this](const QHttpServerRequest &request) { - ProxyLogger::getInstance().debug("Handling POST /api/v1/configs request"); - return handleAddConfigs(request); - }); - - m_server.route("/api/v1/configs", QHttpServerRequest::Method::Put, - [this](const QHttpServerRequest &request) { - ProxyLogger::getInstance().debug("Handling PUT /api/v1/configs request"); - return handleUpdateConfigs(request); - }); - - m_server.route("/api/v1/configs", QHttpServerRequest::Method::Delete, - [this](const QHttpServerRequest &request) { - ProxyLogger::getInstance().debug("Handling DELETE /api/v1/configs request"); - return handleDeleteConfig(request); - }); - - m_server.route("/api/v1/configs/activate", QHttpServerRequest::Method::Put, - [this](const QHttpServerRequest &request) { - ProxyLogger::getInstance().debug("Handling PUT /api/v1/configs/activate request"); - return handleActivateConfig(request); - }); - - m_server.route("/api/v1/configs/active", QHttpServerRequest::Method::Get, - [this](const QHttpServerRequest &request) { - ProxyLogger::getInstance().debug("Handling GET /api/v1/configs/active request"); - return handleGetActiveConfig(request); - }); - - // Xray control routes m_server.route("/api/v1/up", QHttpServerRequest::Method::Post, - [this] { + [this] { ProxyLogger::getInstance().debug("Handling POST /api/v1/up request"); - return handlePostUp(); + return handlePostUp(); }); m_server.route("/api/v1/down", QHttpServerRequest::Method::Post, - [this] { + [this] { ProxyLogger::getInstance().debug("Handling POST /api/v1/down request"); - return handlePostDown(); + return handlePostDown(); }); m_server.route("/api/v1/ping", QHttpServerRequest::Method::Get, - [this] { + [this] { ProxyLogger::getInstance().debug("Handling GET /api/v1/ping request"); - return handleGetPing(); + return handleGetPing(); }); } -QJsonObject HttpApi::handlePostUp() +QHttpServerResponse HttpApi::handlePostUp() { - QJsonObject response; - if (auto service = m_service.lock()) { - if (service->startXray()) { - ProxyLogger::getInstance().info("Xray process started successfully"); - response["status"] = "success"; - response["message"] = "Xray process started successfully"; - - // Try to get port from inbounds configuration - QJsonObject config = service->getConfig(); - if (config.contains("inbounds") && config["inbounds"].isArray()) { - QJsonArray inbounds = config["inbounds"].toArray(); - if (!inbounds.isEmpty() && inbounds[0].isObject()) { - QJsonObject firstInbound = inbounds[0].toObject(); - if (firstInbound.contains("port")) { - int port = firstInbound["port"].toInt(); - ProxyLogger::getInstance().info(QString("Xray listening on port %1").arg(port)); - response["xray_port"] = port; - } - } + const bool started = service->startXray(); + QJsonObject response; + response["status"] = started ? "ok" : "error"; + + const auto port = started ? extractInboundPort(service->getConfig()) : std::optional{}; + response["proxyPort"] = proxyPortValue(port); + + if (started) { + if (port.has_value()) { + ProxyLogger::getInstance().info(QString("Xray process started on port %1").arg(*port)); + } else { + ProxyLogger::getInstance().warning("Xray started but inbound port is unknown (local proxy owner may be missing)"); } } else { - ProxyLogger::getInstance().error("Failed to start Xray process"); - response["status"] = "error"; - response["message"] = "Failed to start xray process"; + ProxyLogger::getInstance().warning("Failed to start Xray process via HTTP API"); } - } else { - ProxyLogger::getInstance().error("Service unavailable while trying to start Xray"); - response["status"] = "error"; - response["message"] = "Service unavailable"; + + return QHttpServerResponse(response); } - return response; + + ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized"); + return makeServiceUnavailablePingResponse(); } -QJsonObject HttpApi::handlePostDown() +QHttpServerResponse HttpApi::handlePostDown() { if (auto service = m_service.lock()) { const bool stopped = service->stopXray(); QJsonObject response; - if (stopped) { - response["status"] = "ok"; - response["description"] = "Xray process stopped"; + response["status"] = stopped ? "ok" : "error"; + if (!stopped) { + ProxyLogger::getInstance().warning("Failed to stop Xray process via HTTP API"); } else { - response["status"] = "error"; - response["description"] = "Failed to stop xray process"; - } - return response; - } - return { {"status", "error"}, {"message", "Service unavailable"} }; -} - -QJsonObject HttpApi::handleGetPing() const -{ - QJsonObject response; - response["status"] = "success"; - response["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); - - if (auto service = m_service.lock()) { - bool isRunning = service->isXrayRunning(); - response["xray_running"] = isRunning; - response["config_count"] = service->getConfigCount(); - - if (isRunning) { - qint64 pid = service->getXrayProcessId(); - ProxyLogger::getInstance().debug(QString("Xray is running (PID: %1)").arg(pid)); - response["xray_pid"] = pid; - response["xray_state"] = "running"; - - QJsonObject config = service->getConfig(); - if (config.contains("inbounds") && config["inbounds"].isArray()) { - QJsonArray inbounds = config["inbounds"].toArray(); - if (!inbounds.isEmpty() && inbounds[0].isObject()) { - QJsonObject firstInbound = inbounds[0].toObject(); - if (firstInbound.contains("port")) { - int port = firstInbound["port"].toInt(); - ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(port)); - response["xray_port"] = port; - } - } - } - - QString error = service->getXrayError(); - if (!error.isEmpty()) { - ProxyLogger::getInstance().warning(QString("Xray error: %1").arg(error)); - response["xray_error"] = error; - } - } else { - ProxyLogger::getInstance().debug("Xray is not running"); - response["xray_state"] = "stopped"; - } - } else { - ProxyLogger::getInstance().error("Service unavailable while processing GET /api/v1/ping request"); - response["status"] = "error"; - response["message"] = "Service unavailable"; - } - - return response; -} - -QHttpServerResponse HttpApi::handleGetConfigs(const QHttpServerRequest &request) -{ - if (auto service = m_service.lock()) { - // Get UUIDs from query parameters if present and decode URL-encoded characters - QString uuidList = QUrl::fromPercentEncoding(request.query().queryItemValue("uuid").toUtf8()); - ProxyLogger::getInstance().debug(QString("UUID filter: %1").arg(uuidList.isEmpty() ? "none" : uuidList)); - - QMap configs; - - if (uuidList.isEmpty()) { - ProxyLogger::getInstance().debug("Retrieving all configs"); - configs = service->getAllConfigs(); - } else { - QStringList uuids = uuidList.split(',', Qt::SkipEmptyParts); - ProxyLogger::getInstance().debug(QString("Retrieving configs for UUIDs: %1").arg(uuids.join(", "))); - configs = service->getConfigsByUuids(uuids); + ProxyLogger::getInstance().info("Xray process stopped via HTTP API"); } - QJsonObject response; - response["status"] = "success"; - - // Convert QMap to QJsonObject manually - QJsonObject configsJson; - for (auto it = configs.constBegin(); it != configs.constEnd(); ++it) { - configsJson[it.key()] = it.value(); - } - response["configs"] = configsJson; - - ProxyLogger::getInstance().info(QString("Successfully retrieved %1 configs").arg(configs.size())); return QHttpServerResponse(response); } - ProxyLogger::getInstance().error("Service unavailable while processing GET /api/v1/configs request"); - return QHttpServerResponse( - QJsonObject{{"status", "error"}, {"message", "Service unavailable"}}, - QHttpServerResponse::StatusCode::ServiceUnavailable - ); + ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized"); + return makeServiceUnavailableStatusResponse(); } -QHttpServerResponse HttpApi::handleAddConfigs(const QHttpServerRequest &request) +QHttpServerResponse HttpApi::handleGetPing() const { - ProxyLogger::getInstance().info("Processing POST /api/v1/configs request"); - if (auto service = m_service.lock()) { - // Parse request body - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(request.body(), &parseError); - - if (parseError.error != QJsonParseError::NoError) { - ProxyLogger::getInstance().error(QString("Invalid JSON format: %1").arg(parseError.errorString())); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Invalid JSON format"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - if (!doc.isObject()) { - ProxyLogger::getInstance().error("Request body is not a JSON object"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Request body must be a JSON object"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - QJsonObject root = doc.object(); - if (!root.contains("configs") || !root["configs"].isArray()) { - ProxyLogger::getInstance().error("Request body missing 'configs' array"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Request body must contain 'configs' array"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - // Convert JSON array to string list - QStringList configs; - QJsonArray configsArray = root["configs"].toArray(); - ProxyLogger::getInstance().debug(QString("Processing %1 configs from request").arg(configsArray.size())); - - for (const auto &value : configsArray) { - if (!value.isString()) { - ProxyLogger::getInstance().error("Invalid config format: config must be a string"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "All configs must be strings"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); + QJsonObject response; + response["status"] = "ok"; + const bool isRunning = service->isXrayRunning(); + if (isRunning) { + const auto port = extractInboundPort(service->getConfig()); + response["proxyPort"] = proxyPortValue(port); + if (port.has_value()) { + ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(*port)); + } else { + ProxyLogger::getInstance().warning("Unable to detect inbound port while Xray is running"); } - configs.append(value.toString()); - } - - if (configs.isEmpty()) { - ProxyLogger::getInstance().error("Empty configs array in request"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Configs array cannot be empty"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - // Add configs - ProxyLogger::getInstance().info(QString("Adding %1 configs").arg(configs.size())); - if (service->addConfigs(configs)) { - ProxyLogger::getInstance().info("Successfully added configs"); - return QHttpServerResponse( - QJsonObject{ - {"status", "success"}, - {"message", "Configs added successfully"} - } - ); } else { - ProxyLogger::getInstance().error("Failed to add configs"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Failed to add configs"} - }, - QHttpServerResponse::StatusCode::InternalServerError - ); + response["proxyPort"] = QJsonValue::Null; + ProxyLogger::getInstance().debug("Xray is not running"); } + + return QHttpServerResponse(response); } - ProxyLogger::getInstance().error("Service unavailable while processing POST /api/v1/configs request"); - return QHttpServerResponse( - QJsonObject{{"status", "error"}, {"message", "Service unavailable"}}, - QHttpServerResponse::StatusCode::ServiceUnavailable - ); + ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized"); + return makeServiceUnavailablePingResponse(); } - -QHttpServerResponse HttpApi::handleUpdateConfigs(const QHttpServerRequest &request) -{ - if (auto service = m_service.lock()) { - // Parse request body - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(request.body(), &parseError); - - if (parseError.error != QJsonParseError::NoError) { - ProxyLogger::getInstance().error(QString("Invalid JSON format: %1").arg(parseError.errorString())); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Invalid JSON format"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - if (!doc.isObject()) { - ProxyLogger::getInstance().error("Request body is not a JSON object"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Request body must be a JSON object"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - QJsonObject root = doc.object(); - if (!root.contains("configs") || !root["configs"].isArray()) { - ProxyLogger::getInstance().error("Request body missing 'configs' array"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Request body must contain 'configs' array"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - // Convert JSON array to string list - QStringList configs; - QJsonArray configsArray = root["configs"].toArray(); - ProxyLogger::getInstance().debug(QString("Processing %1 configs from request").arg(configsArray.size())); - - for (const auto &value : configsArray) { - if (!value.isString()) { - ProxyLogger::getInstance().error("Invalid config format: config must be a string"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "All configs must be strings"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - configs.append(value.toString()); - } - - if (configs.isEmpty()) { - ProxyLogger::getInstance().error("Empty configs array in request"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Configs array cannot be empty"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - // Update configs - ProxyLogger::getInstance().info(QString("Updating all configs with %1 new config(s)").arg(configs.size())); - if (service->updateAllConfigs(configs)) { - ProxyLogger::getInstance().info("Successfully updated all configs"); - return QHttpServerResponse( - QJsonObject{ - {"status", "success"}, - {"message", "Configs updated successfully"} - } - ); - } else { - ProxyLogger::getInstance().error("Failed to update configs"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Failed to update configs"} - }, - QHttpServerResponse::StatusCode::InternalServerError - ); - } - } - - ProxyLogger::getInstance().error("Service unavailable while processing PUT /api/v1/configs request"); - return QHttpServerResponse( - QJsonObject{{"status", "error"}, {"message", "Service unavailable"}}, - QHttpServerResponse::StatusCode::ServiceUnavailable - ); -} - -QHttpServerResponse HttpApi::handleDeleteConfig(const QHttpServerRequest &request) -{ - if (auto service = m_service.lock()) { - // Get UUID from query parameters - QString uuid = request.query().queryItemValue("uuid"); - - if (uuid.isEmpty()) { - ProxyLogger::getInstance().error("Missing UUID parameter in request"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "UUID parameter is required"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - ProxyLogger::getInstance().info(QString("Attempting to delete config with UUID: %1").arg(uuid)); - // Delete config - if (service->removeConfig(uuid)) { - ProxyLogger::getInstance().info(QString("Successfully deleted config with UUID: %1").arg(uuid)); - return QHttpServerResponse( - QJsonObject{ - {"status", "success"}, - {"message", "Config deleted successfully"} - } - ); - } else { - ProxyLogger::getInstance().error(QString("Failed to delete config with UUID: %1").arg(uuid)); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Failed to delete config"} - }, - QHttpServerResponse::StatusCode::InternalServerError - ); - } - } - - ProxyLogger::getInstance().error("Service unavailable while processing DELETE /api/v1/configs request"); - return QHttpServerResponse( - QJsonObject{{"status", "error"}, {"message", "Service unavailable"}}, - QHttpServerResponse::StatusCode::ServiceUnavailable - ); -} - -QHttpServerResponse HttpApi::handleActivateConfig(const QHttpServerRequest &request) -{ - if (auto service = m_service.lock()) { - // Get UUID from query parameters - QString uuid = request.query().queryItemValue("uuid"); - - if (uuid.isEmpty()) { - ProxyLogger::getInstance().error("Missing UUID parameter in request"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "UUID parameter is required"} - }, - QHttpServerResponse::StatusCode::BadRequest - ); - } - - ProxyLogger::getInstance().info(QString("Attempting to activate config with UUID: %1").arg(uuid)); - // Activate config - if (service->activateConfig(uuid)) { - ProxyLogger::getInstance().info(QString("Successfully activated config with UUID: %1").arg(uuid)); - return QHttpServerResponse( - QJsonObject{ - {"status", "success"}, - {"message", "Config activated successfully"} - } - ); - } else { - ProxyLogger::getInstance().error(QString("Failed to activate config with UUID: %1").arg(uuid)); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "Failed to activate config"} - }, - QHttpServerResponse::StatusCode::InternalServerError - ); - } - } - - ProxyLogger::getInstance().error("Service unavailable while processing PUT /api/v1/configs/activate request"); - return QHttpServerResponse( - QJsonObject{{"status", "error"}, {"message", "Service unavailable"}}, - QHttpServerResponse::StatusCode::ServiceUnavailable - ); -} - -QHttpServerResponse HttpApi::handleGetActiveConfig(const QHttpServerRequest &request) -{ - if (auto service = m_service.lock()) { - QJsonObject activeConfig = service->getActiveConfig(); - - if (!activeConfig.isEmpty()) { - ProxyLogger::getInstance().info("Successfully retrieved active config"); - QJsonObject response; - response["status"] = "success"; - response["config"] = activeConfig; - return QHttpServerResponse(response); - } else { - ProxyLogger::getInstance().warning("No active config found"); - return QHttpServerResponse( - QJsonObject{ - {"status", "error"}, - {"message", "No active config found"} - }, - QHttpServerResponse::StatusCode::NotFound - ); - } - } - - ProxyLogger::getInstance().error("Service unavailable while processing GET /api/v1/configs/active request"); - return QHttpServerResponse( - QJsonObject{{"status", "error"}, {"message", "Service unavailable"}}, - QHttpServerResponse::StatusCode::ServiceUnavailable - ); -} \ No newline at end of file diff --git a/client/core/local-proxy/httpapi.h b/client/core/local-proxy/httpapi.h index 4ec3c09e8..dfc45bc68 100644 --- a/client/core/local-proxy/httpapi.h +++ b/client/core/local-proxy/httpapi.h @@ -21,19 +21,10 @@ public: private: void setupRoutes(); - - // Config management endpoints - QHttpServerResponse handleGetConfigs(const QHttpServerRequest &request); - QHttpServerResponse handleAddConfigs(const QHttpServerRequest &request); - QHttpServerResponse handleUpdateConfigs(const QHttpServerRequest &request); - QHttpServerResponse handleDeleteConfig(const QHttpServerRequest &request); - QHttpServerResponse handleActivateConfig(const QHttpServerRequest &request); - QHttpServerResponse handleGetActiveConfig(const QHttpServerRequest &request); - // Xray control endpoints - QJsonObject handlePostUp(); - QJsonObject handlePostDown(); - QJsonObject handleGetPing() const; + QHttpServerResponse handlePostUp(); + QHttpServerResponse handlePostDown(); + QHttpServerResponse handleGetPing() const; QHttpServer m_server; QScopedPointer m_tcpServer;