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.
This commit is contained in:
aiamnezia
2025-12-31 19:30:29 +04:00
parent 9740e7557a
commit 806b1d75af
2 changed files with 101 additions and 501 deletions
+96 -487
View File
@@ -2,11 +2,56 @@
#include "proxylogger.h" #include "proxylogger.h"
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument>
#include <QDateTime>
#include <QHostAddress> #include <QHostAddress>
#include <QDebug> #include <optional>
#include <QUrl>
namespace {
std::optional<int> 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<int> &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<IProxyService> service, QObject* parent) HttpApi::HttpApi(QWeakPointer<IProxyService> service, QObject* parent)
: 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().info(QString("HTTP API server is running on localhost:%1").arg(m_tcpServer->serverPort()));
ProxyLogger::getInstance().debug("Available endpoints:\n" 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/up\n"
" POST /api/v1/down\n" " POST /api/v1/down\n"
" GET /api/v1/ping"); " GET /api/v1/ping");
@@ -59,44 +98,6 @@ void HttpApi::setupRoutes()
{ {
ProxyLogger::getInstance().debug("Setting up HTTP API routes"); 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, m_server.route("/api/v1/up", QHttpServerRequest::Method::Post,
[this] { [this] {
ProxyLogger::getInstance().debug("Handling POST /api/v1/up request"); ProxyLogger::getInstance().debug("Handling POST /api/v1/up request");
@@ -116,466 +117,74 @@ void HttpApi::setupRoutes()
}); });
} }
QJsonObject HttpApi::handlePostUp() QHttpServerResponse HttpApi::handlePostUp()
{ {
QJsonObject response;
if (auto service = m_service.lock()) { if (auto service = m_service.lock()) {
if (service->startXray()) { const bool started = service->startXray();
ProxyLogger::getInstance().info("Xray process started successfully"); QJsonObject response;
response["status"] = "success"; response["status"] = started ? "ok" : "error";
response["message"] = "Xray process started successfully";
// Try to get port from inbounds configuration const auto port = started ? extractInboundPort(service->getConfig()) : std::optional<int>{};
QJsonObject config = service->getConfig(); response["proxyPort"] = proxyPortValue(port);
if (config.contains("inbounds") && config["inbounds"].isArray()) {
QJsonArray inbounds = config["inbounds"].toArray(); if (started) {
if (!inbounds.isEmpty() && inbounds[0].isObject()) { if (port.has_value()) {
QJsonObject firstInbound = inbounds[0].toObject(); ProxyLogger::getInstance().info(QString("Xray process started on port %1").arg(*port));
if (firstInbound.contains("port")) { } else {
int port = firstInbound["port"].toInt(); ProxyLogger::getInstance().warning("Xray started but inbound port is unknown (local proxy owner may be missing)");
ProxyLogger::getInstance().info(QString("Xray listening on port %1").arg(port));
response["xray_port"] = port;
}
}
} }
} else { } else {
ProxyLogger::getInstance().error("Failed to start Xray process"); ProxyLogger::getInstance().warning("Failed to start Xray process via HTTP API");
response["status"] = "error";
response["message"] = "Failed to start xray process";
} }
} else {
ProxyLogger::getInstance().error("Service unavailable while trying to start Xray"); return QHttpServerResponse(response);
response["status"] = "error";
response["message"] = "Service unavailable";
} }
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()) { if (auto service = m_service.lock()) {
const bool stopped = service->stopXray(); const bool stopped = service->stopXray();
QJsonObject response; QJsonObject response;
if (stopped) { response["status"] = stopped ? "ok" : "error";
if (!stopped) {
ProxyLogger::getInstance().warning("Failed to stop Xray process via HTTP API");
} else {
ProxyLogger::getInstance().info("Xray process stopped via HTTP API");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailableStatusResponse();
}
QHttpServerResponse HttpApi::handleGetPing() const
{
if (auto service = m_service.lock()) {
QJsonObject response;
response["status"] = "ok"; response["status"] = "ok";
response["description"] = "Xray process stopped"; const bool isRunning = service->isXrayRunning();
} 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) { if (isRunning) {
qint64 pid = service->getXrayProcessId(); const auto port = extractInboundPort(service->getConfig());
ProxyLogger::getInstance().debug(QString("Xray is running (PID: %1)").arg(pid)); response["proxyPort"] = proxyPortValue(port);
response["xray_pid"] = pid; if (port.has_value()) {
response["xray_state"] = "running"; ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(*port));
} else {
QJsonObject config = service->getConfig(); ProxyLogger::getInstance().warning("Unable to detect inbound port while Xray is running");
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 { } else {
response["proxyPort"] = QJsonValue::Null;
ProxyLogger::getInstance().debug("Xray is not running"); 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<QString, QJsonObject> 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);
}
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); return QHttpServerResponse(response);
} }
ProxyLogger::getInstance().error("Service unavailable while processing GET /api/v1/configs request"); ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return QHttpServerResponse( return makeServiceUnavailablePingResponse();
QJsonObject{{"status", "error"}, {"message", "Service unavailable"}},
QHttpServerResponse::StatusCode::ServiceUnavailable
);
}
QHttpServerResponse HttpApi::handleAddConfigs(const QHttpServerRequest &request)
{
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
);
}
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
);
}
}
ProxyLogger::getInstance().error("Service unavailable while processing POST /api/v1/configs request");
return QHttpServerResponse(
QJsonObject{{"status", "error"}, {"message", "Service unavailable"}},
QHttpServerResponse::StatusCode::ServiceUnavailable
);
}
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
);
} }
+3 -12
View File
@@ -22,18 +22,9 @@ public:
private: private:
void setupRoutes(); void setupRoutes();
// Config management endpoints QHttpServerResponse handlePostUp();
QHttpServerResponse handleGetConfigs(const QHttpServerRequest &request); QHttpServerResponse handlePostDown();
QHttpServerResponse handleAddConfigs(const QHttpServerRequest &request); QHttpServerResponse handleGetPing() const;
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;
QHttpServer m_server; QHttpServer m_server;
QScopedPointer<QTcpServer> m_tcpServer; QScopedPointer<QTcpServer> m_tcpServer;