use std::collections::HashMap;
use std::net::SocketAddr;
use tokio::io::{AsyncBufReadExt, AsyncReadExt};
const MAX_PRELUDE_LINE_BYTES: usize = 8 * 1024;
const MAX_PRELUDE_BYTES: usize = 64 * 1024;
const MAX_PRELUDE_VARIABLES: usize = 128;
#[derive(Debug, Clone)]
pub struct AgiRequest {
variables: HashMap<String, String>,
peer_addr: Option<SocketAddr>,
}
impl AgiRequest {
pub async fn parse_from_reader<R: tokio::io::AsyncBufRead + Unpin>(
reader: &mut R,
) -> crate::error::Result<Self> {
let mut variables = HashMap::new();
let mut line = String::new();
let mut total_bytes = 0usize;
loop {
line.clear();
let bytes_read = (&mut *reader)
.take((MAX_PRELUDE_LINE_BYTES + 1) as u64)
.read_line(&mut line)
.await?;
total_bytes = total_bytes.saturating_add(bytes_read);
if total_bytes > MAX_PRELUDE_BYTES {
return Err(invalid_request("AGI prelude exceeds 65536 bytes"));
}
if bytes_read == 0 {
return Err(invalid_request(
"AGI prelude ended before its blank-line terminator",
));
}
if line.len() > MAX_PRELUDE_LINE_BYTES {
return Err(invalid_request("AGI prelude line exceeds 8192 bytes"));
}
if !line.ends_with('\n') {
return Err(invalid_request("AGI prelude line ended without a newline"));
}
let content = line.strip_suffix('\n').expect("line ending checked above");
let content = content.strip_suffix('\r').unwrap_or(content);
if content.is_empty() {
break;
}
if content.contains('\r') || content.contains('\0') {
return Err(invalid_request(
"AGI prelude line contains a forbidden control character",
));
}
let (key, value) = content
.split_once(':')
.ok_or_else(|| invalid_request("AGI prelude line is missing ':'"))?;
let key = key.trim();
if key.is_empty() {
return Err(invalid_request("AGI prelude variable name is empty"));
}
let value = value.strip_prefix(' ').unwrap_or(value);
let key = key.strip_prefix("agi_").unwrap_or(key);
if key.is_empty() {
return Err(invalid_request("AGI prelude variable name is empty"));
}
if !variables.contains_key(key) && variables.len() >= MAX_PRELUDE_VARIABLES {
return Err(invalid_request("AGI prelude exceeds 128 variables"));
}
variables.insert(key.to_owned(), value.to_owned());
}
Ok(Self {
variables,
peer_addr: None,
})
}
pub(crate) fn set_peer_addr(&mut self, peer_addr: SocketAddr) {
self.peer_addr = Some(peer_addr);
}
pub fn peer_addr(&self) -> Option<SocketAddr> {
self.peer_addr
}
pub fn network(&self) -> Option<&str> {
self.variables.get("network").map(String::as_str)
}
pub fn network_script(&self) -> Option<&str> {
self.variables.get("network_script").map(String::as_str)
}
pub fn request(&self) -> Option<&str> {
self.variables.get("request").map(String::as_str)
}
pub fn channel(&self) -> Option<&str> {
self.variables.get("channel").map(String::as_str)
}
pub fn language(&self) -> Option<&str> {
self.variables.get("language").map(String::as_str)
}
pub fn channel_type(&self) -> Option<&str> {
self.variables.get("type").map(String::as_str)
}
pub fn unique_id(&self) -> Option<&str> {
self.variables.get("uniqueid").map(String::as_str)
}
pub fn caller_id(&self) -> Option<&str> {
self.variables.get("callerid").map(String::as_str)
}
pub fn caller_id_name(&self) -> Option<&str> {
self.variables.get("calleridname").map(String::as_str)
}
pub fn context(&self) -> Option<&str> {
self.variables.get("context").map(String::as_str)
}
pub fn extension(&self) -> Option<&str> {
self.variables.get("extension").map(String::as_str)
}
pub fn priority(&self) -> Option<&str> {
self.variables.get("priority").map(String::as_str)
}
pub fn get(&self, key: &str) -> Option<&str> {
self.variables.get(key).map(String::as_str)
}
}
fn invalid_request(details: &'static str) -> crate::error::AgiError {
crate::error::AgiError::InvalidRequest {
details: details.to_owned(),
}
}