use std::io::BufRead;
use anyhow::{bail, Context, Result};
use owo_colors::OwoColorize;
use crate::api::client::ApiClient;
use crate::api::models::EdgeAccessPage;
use crate::exit;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::theme;
pub async fn maintenance(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
enabled: bool,
message: Option<&str>,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let answer = client
.set_maintenance(&service.id, enabled, message)
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
if answer.body.maintenance.enabled {
println!(
" {} {} está en mantenimiento",
theme::CHECK.style(theme::success()),
sanitise::plain(&service.name).style(theme::strong()),
);
println!(
" {}",
"Quien entre verá la página de mantenimiento; el servicio sigue en marcha. \
`fundaia maintenance … --off` lo vuelve a servir."
.style(theme::muted()),
);
} else {
println!(
" {} {} vuelve a responder",
theme::CHECK.style(theme::success()),
sanitise::plain(&service.name).style(theme::strong()),
);
}
Ok(())
}
pub async fn protect(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
username: &str,
password: Option<&str>,
format: Format,
) -> Result<()> {
let password = match password {
Some(given) => given.to_owned(),
None => read_password(format)?,
};
let (_, service) = client.locate(project_reference, service_reference).await?;
let answer = client
.protect_service(&service.id, username, &password)
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
println!(
" {} {} pide usuario y contraseña ({})",
theme::CHECK.style(theme::success()),
sanitise::plain(&service.name).style(theme::strong()),
sanitise::plain(
answer
.body
.protection
.username
.as_deref()
.unwrap_or(username)
)
.style(theme::muted()),
);
Ok(())
}
pub async fn unprotect(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let answer = client.unprotect_service(&service.id).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
println!(
" {} {} ya no pide contraseña",
theme::CHECK.style(theme::success()),
sanitise::plain(&service.name).style(theme::strong()),
);
Ok(())
}
pub struct AllowRequest<'a> {
pub entries: &'a [String],
pub add: bool,
pub clear: bool,
pub list: bool,
}
pub async fn allow(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
request: AllowRequest<'_>,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let answer = if request.list {
client.edge_access(&service.id).await?
} else if request.clear {
client.clear_allowlist(&service.id).await?
} else {
let mut entries: Vec<String> = Vec::new();
if request.add {
let current = client.edge_access(&service.id).await?;
entries.extend(
current
.body
.ip_allowlist
.map(|list| list.entries)
.unwrap_or_default(),
);
}
entries.extend(request.entries.iter().cloned());
client.set_allowlist(&service.id, &entries).await?
};
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let name = sanitise::plain(&service.name);
match answer
.body
.ip_allowlist
.as_ref()
.filter(|list| list.enabled)
{
Some(list) => {
println!(
" {} {} solo deja entrar a {}:",
if request.list {
theme::ARROW.style(theme::accent()).to_string()
} else {
theme::CHECK.style(theme::success()).to_string()
},
name.style(theme::strong()),
if list.entries.len() == 1 {
"una dirección".to_owned()
} else {
format!("{} direcciones o redes", list.entries.len())
},
);
for entry in &list.entries {
println!(" {}", sanitise::plain(entry));
}
println!(
" {}",
"Las demás reciben un 403, también quien llega sin una dirección conocida."
.style(theme::muted()),
);
}
None => println!(
" {} {} deja entrar a cualquiera: no tiene lista de direcciones IP",
theme::CHECK.style(theme::success()),
name.style(theme::strong()),
),
}
if request.list {
print_rate_limit(&answer.body);
}
Ok(())
}
pub async fn rate_limit(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
rate: &str,
format: Format,
) -> Result<()> {
let wanted = rate_of(rate)?;
let (_, service) = client.locate(project_reference, service_reference).await?;
let answer = match wanted {
Some(per_minute) => client.set_rate_limit(&service.id, per_minute).await?,
None => client.clear_rate_limit(&service.id).await?,
};
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let name = sanitise::plain(&service.name);
match limit_of(&answer.body) {
Some(per_minute) => println!(
" {} {} admite {per_minute} peticiones por minuto de cada dirección; las demás reciben un 429",
theme::CHECK.style(theme::success()),
name.style(theme::strong()),
),
None => println!(
" {} {} ya no limita las peticiones",
theme::CHECK.style(theme::success()),
name.style(theme::strong()),
),
}
Ok(())
}
fn print_rate_limit(edge: &EdgeAccessPage) {
if let Some(per_minute) = limit_of(edge) {
println!(
" {} {per_minute} peticiones por minuto y dirección",
"límite".style(theme::muted()),
);
}
}
fn limit_of(edge: &EdgeAccessPage) -> Option<u32> {
edge.rate_limit
.as_ref()
.filter(|limit| limit.enabled)
.and_then(|limit| limit.per_minute)
}
pub fn rate_of(text: &str) -> Result<Option<u32>> {
let written = text.trim().to_ascii_lowercase();
if written == "off" {
return Ok(None);
}
let number = written
.strip_suffix("/min")
.or_else(|| written.strip_suffix("/m"))
.unwrap_or(&written)
.trim();
match number.parse::<u32>() {
Ok(per_minute) if (1..=100_000).contains(&per_minute) => Ok(Some(per_minute)),
_ => Err(exit::Refused::invalid(format!(
"`{}` no es un límite: escribe cuántas peticiones por minuto (600/min, de 1 a 100000) u off",
sanitise::plain(text)
))
.into()),
}
}
fn read_password(format: Format) -> Result<String> {
let interactive = !format.is_json() && std::io::IsTerminal::is_terminal(&std::io::stdin());
let password = if interactive {
eprint!(" Contraseña (no se ve al escribir): ");
rpassword::read_password().context("no se pudo leer la contraseña del teclado")?
} else {
let mut line = String::new();
std::io::stdin()
.lock()
.read_line(&mut line)
.context("no se pudo leer la contraseña de la entrada")?;
line
};
let password = password.trim_end_matches(['\r', '\n']).to_owned();
if password.is_empty() {
bail!("sin contraseña no se protege nada: pásala con --password, FUNDAIA_SERVICE_PASSWORD o por la entrada");
}
Ok(password)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_read_a_limit_per_minute() {
assert_eq!(rate_of("600/min").ok(), Some(Some(600)));
}
#[test]
fn it_should_read_a_bare_number_as_per_minute() {
assert_eq!(rate_of("120").ok(), Some(Some(120)));
}
#[test]
fn it_should_read_off_as_no_limit() {
assert_eq!(rate_of("OFF").ok(), Some(None));
}
#[test]
fn it_should_refuse_a_limit_of_nothing() {
assert!(rate_of("0/min").is_err());
}
#[test]
fn it_should_refuse_a_limit_per_hour_as_a_validation_failure() {
let refused = rate_of("600/h").expect_err("a rate per hour is not one per minute");
assert_eq!(exit::classify(&refused).code(), 4);
}
}