use crate::api::response;
use crate::api::schemas::ports::{
PortCreated, PortCreatedResponse, PortDetail, PortDetailResponse, PortInfo, PortListResponse,
ProtocolStatus,
};
use crate::api::utils::config_file;
use crate::common::{config::file_loader, net::port_utils};
use crate::ingress::state::Protocol;
use crate::layers::l4::fs as transport_fs;
use axum::{extract::Path, http::StatusCode, response::IntoResponse};
use tokio::fs;
#[utoipa::path(
get,
path = "/ports",
responses(
(status = 200, description = "List of ports", body = PortListResponse)
),
tag = "ports",
security(("bearer_auth" = []))
)]
pub async fn list_ports_handler() -> impl IntoResponse {
let config_dir = file_loader::get_config_dir();
let mut ports = Vec::new();
let config = crate::config::get();
let tcp_map = config.listeners.tcp.snapshot().await;
let udp_map = config.listeners.udp.snapshot().await;
let mut entries = match fs::read_dir(&config_dir).await {
Ok(entries) => entries,
Err(e) => {
return response::error(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to read config directory: {e}"),
);
}
};
while let Ok(Some(entry)) = entries.next_entry().await {
if let Ok(metadata) = entry.metadata().await {
if !metadata.is_dir() {
continue;
}
} else {
continue;
}
if let Some(name) = entry.file_name().to_str()
&& name.starts_with('[')
&& name.ends_with(']')
&& let Ok(port_num) = name[1..name.len() - 1].parse::<u16>()
{
let port_str = port_num.to_string();
let active = tcp_map.contains_key(&port_str) || udp_map.contains_key(&port_str);
let mut protocols = Vec::new();
let tcp_dir = config_dir.join(name).join("tcp");
if fs::metadata(&tcp_dir).await.is_ok() {
protocols.push("tcp".to_owned());
}
let udp_dir = config_dir.join(name).join("udp");
if fs::metadata(&udp_dir).await.is_ok() {
protocols.push("udp".to_owned());
}
ports.push(PortInfo {
port: port_num,
protocols,
active,
});
}
}
ports.sort_by_key(|p| p.port);
response::success(ports)
}
#[utoipa::path(
get,
path = "/ports/{port}",
params(
("port" = u16, Path, description = "Port number")
),
responses(
(status = 200, description = "Port details", body = PortDetailResponse),
(status = 404, description = "Port not configured")
),
tag = "ports",
security(("bearer_auth" = []))
)]
pub async fn get_port_handler(Path(port): Path<u16>) -> impl IntoResponse {
let port_dir = file_loader::get_config_dir().join(format!("[{port}]"));
if fs::metadata(&port_dir).await.is_err() {
return response::error(StatusCode::NOT_FOUND, format!("Port {port} not configured"));
}
let config = crate::config::get();
let port_str = port.to_string();
let tcp_path = port_dir.join("tcp");
let tcp = if fs::metadata(&tcp_path).await.is_ok() {
let config_res = config_file::find_config::<serde_json::Value>(&tcp_path).await;
let source_format = match config_res {
config_file::ConfigFileResult::Single { format, .. } => Some(format),
_ => None,
};
let active = config.listeners.get_tcp(&port_str).is_some();
Some(ProtocolStatus {
active,
source_format,
})
} else {
None
};
let udp_path = port_dir.join("udp");
let udp = if fs::metadata(&udp_path).await.is_ok() {
let config_res = config_file::find_config::<serde_json::Value>(&udp_path).await;
let source_format = match config_res {
config_file::ConfigFileResult::Single { format, .. } => Some(format),
_ => None,
};
let active = config.listeners.get_udp(&port_str).is_some();
Some(ProtocolStatus {
active,
source_format,
})
} else {
None
};
response::success(PortDetail { port, tcp, udp })
}
#[utoipa::path(
post,
path = "/ports/{port}",
params(
("port" = u16, Path, description = "Port number")
),
responses(
(status = 201, description = "Port created", body = PortCreatedResponse),
(status = 400, description = "Invalid port"),
(status = 409, description = "Port already exists")
),
tag = "ports",
security(("bearer_auth" = []))
)]
pub async fn create_port_handler(Path(port): Path<u16>) -> impl IntoResponse {
if !port_utils::is_valid_port(port) {
return response::error(StatusCode::BAD_REQUEST, "Invalid port number".into());
}
let port_dir = file_loader::get_config_dir().join(format!("[{port}]"));
if fs::metadata(&port_dir).await.is_ok() {
return response::error(StatusCode::CONFLICT, format!("Port {port} already exists"));
}
match fs::create_dir(&port_dir).await {
Ok(_) => response::created(PortCreated {
port,
created: true,
}),
Err(e) => response::error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
#[utoipa::path(
delete,
path = "/ports/{port}",
params(
("port" = u16, Path, description = "Port number")
),
responses(
(status = 204, description = "Port deleted"),
(status = 404, description = "Port not found")
),
tag = "ports",
security(("bearer_auth" = []))
)]
pub async fn delete_port_handler(Path(port): Path<u16>) -> impl IntoResponse {
let port_dir = file_loader::get_config_dir().join(format!("[{port}]"));
if fs::metadata(&port_dir).await.is_err() {
return response::error(StatusCode::NOT_FOUND, format!("Port {port} not found"));
}
match fs::remove_dir_all(&port_dir).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => response::error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
#[utoipa::path(
post,
path = "/ports/{port}/{protocol}",
params(
("port" = u16, Path, description = "Port number"),
("protocol" = String, Path, description = "Protocol (tcp/udp)")
),
responses(
(status = 201, description = "Protocol enabled"),
(status = 400, description = "Invalid protocol")
),
tag = "ports",
security(("bearer_auth" = []))
)]
pub async fn enable_protocol_handler(
Path((port, protocol_str)): Path<(u16, String)>,
) -> impl IntoResponse {
let protocol = match protocol_str.as_str() {
"tcp" => Protocol::Tcp,
"udp" => Protocol::Udp,
_ => return response::error(StatusCode::BAD_REQUEST, "Invalid protocol".into()),
};
match transport_fs::create_protocol_listener(port, &protocol).await {
Ok(_) => StatusCode::CREATED.into_response(),
Err(e) => response::error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
#[utoipa::path(
delete,
path = "/ports/{port}/{protocol}",
params(
("port" = u16, Path, description = "Port number"),
("protocol" = String, Path, description = "Protocol (tcp/udp)")
),
responses(
(status = 204, description = "Protocol disabled"),
(status = 400, description = "Invalid protocol")
),
tag = "ports",
security(("bearer_auth" = []))
)]
pub async fn disable_protocol_handler(
Path((port, protocol_str)): Path<(u16, String)>,
) -> impl IntoResponse {
let protocol = match protocol_str.as_str() {
"tcp" => Protocol::Tcp,
"udp" => Protocol::Udp,
_ => return response::error(StatusCode::BAD_REQUEST, "Invalid protocol".into()),
};
match transport_fs::delete_protocol_listener(port, &protocol).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => response::error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}