use axum::http::{Uri, uri::Authority};
use std::{collections::BTreeSet, time::Duration};
use thiserror::Error;
#[derive(Clone, Debug)]
pub struct AgentHttpOptions {
pub(super) hosts: BTreeSet<String>,
pub(super) origins: BTreeSet<String>,
pub(super) max_request_bytes: usize,
pub(super) max_response_bytes: usize,
pub(super) max_in_flight: usize,
pub(super) deadline: Duration,
pub(super) sse: Option<super::AgentHttpSseOptions>,
}
impl AgentHttpOptions {
pub fn new(hosts: impl IntoIterator<Item = String>) -> Result<Self, AgentHttpOptionsError> {
let mut allowed = BTreeSet::new();
for host in hosts {
if allowed.len() == 64
|| host.len() > 255
|| host.contains(['@', '*'])
|| host.trim() != host
{
return Err(AgentHttpOptionsError);
}
let authority = host
.parse::<Authority>()
.map_err(|_| AgentHttpOptionsError)?;
if !valid_authority(&authority) || !allowed.insert(host.to_ascii_lowercase()) {
return Err(AgentHttpOptionsError);
}
}
if allowed.is_empty() {
return Err(AgentHttpOptionsError);
}
Ok(Self {
hosts: allowed,
origins: BTreeSet::new(),
max_request_bytes: 256 * 1024,
max_response_bytes: 2 * 1024 * 1024,
max_in_flight: 64,
deadline: Duration::from_secs(15),
sse: None,
})
}
pub fn loopback(port: u16) -> Result<Self, AgentHttpOptionsError> {
if port == 0 {
return Err(AgentHttpOptionsError);
}
Self::new([
format!("127.0.0.1:{port}"),
format!("localhost:{port}"),
format!("[::1]:{port}"),
])
}
pub fn with_allowed_origins(
mut self,
origins: impl IntoIterator<Item = String>,
) -> Result<Self, AgentHttpOptionsError> {
let mut allowed = BTreeSet::new();
for origin in origins {
let uri = origin.parse::<Uri>().map_err(|_| AgentHttpOptionsError)?;
let authority = uri.authority().ok_or(AgentHttpOptionsError)?;
if allowed.len() == 64
|| origin.len() > 512
|| uri.scheme_str() != Some("https")
|| origin != format!("https://{authority}")
|| authority.as_str().contains(['@', '*'])
|| !valid_authority(authority)
|| !allowed.insert(origin)
{
return Err(AgentHttpOptionsError);
}
}
self.origins = allowed;
Ok(self)
}
pub fn with_limits(
mut self,
request_bytes: usize,
response_bytes: usize,
in_flight: usize,
) -> Result<Self, AgentHttpOptionsError> {
if !(1..=2 * 1024 * 1024).contains(&request_bytes)
|| !(1024..=8 * 1024 * 1024).contains(&response_bytes)
|| !(1..=1024).contains(&in_flight)
{
return Err(AgentHttpOptionsError);
}
self.max_request_bytes = request_bytes;
self.max_response_bytes = response_bytes;
self.max_in_flight = in_flight;
Ok(self)
}
pub fn with_deadline(mut self, deadline: Duration) -> Result<Self, AgentHttpOptionsError> {
if !(Duration::from_millis(10)..=Duration::from_secs(60)).contains(&deadline) {
return Err(AgentHttpOptionsError);
}
self.deadline = deadline;
Ok(self)
}
#[must_use]
pub fn with_sse(mut self, options: super::AgentHttpSseOptions) -> Self {
self.sse = Some(options);
self
}
}
fn valid_authority(authority: &Authority) -> bool {
let host = authority.host();
if host.is_empty() {
return false;
}
let suffix = &authority.as_str()[host.len()..];
suffix.is_empty()
|| (suffix.starts_with(':') && authority.port_u16().is_some_and(|port| port != 0))
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[error("invalid Agent HTTP deployment policy")]
pub struct AgentHttpOptionsError;