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

View File

@@ -2,11 +2,56 @@
#include "proxylogger.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QDateTime>
#include <QHostAddress>
#include <QDebug>
#include <QUrl>
#include <optional>
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)
: 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<int>{};
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<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);
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
);
}

View File

@@ -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<QTcpServer> m_tcpServer;