diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d353b05..9278038 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,7 +2,7 @@ name: Rust on: push: - branches: [main] + branches: "*" pull_request: branches: [main] @@ -14,6 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Test run: cargo test --all-features --all-targets --verbose diff --git a/Cargo.toml b/Cargo.toml index be5426f..08003e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,15 @@ repository = "https://github.com/KaranGauswami/freeswitch-esl.git" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1", features = ["io-util", "net", "rt"] } +tokio = { version = "1", features = ["io-util", "net", "rt","sync"] } tracing = "0.1" -bytes = "1.4" -tokio-util = { version = "0.7", features = ["codec"] } -tokio-stream = "0.1" -futures = "0.3" -serde_json = "1.0" uuid = { version = "1.4", features = ["v4"] } thiserror = "1.0" +nom = "7.1" +dashmap = "5.5" [dev-dependencies] -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -anyhow = "*" -regex ="*" -ntest = "0.9.0" \ No newline at end of file +tokio = { version = "1.32", features = ["rt-multi-thread", "macros"] } +anyhow = "1.0" +regex ="1.9" +ntest = "0.9.0" diff --git a/README.md b/README.md index 5541f92..69d64cd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# UNSTABLE BRANCH + # freeswitch-esl (WIP) ![workflow](https://github.com/KaranGauswami/freeswitch-esl/actions/workflows/rust.yml/badge.svg) @@ -12,12 +14,14 @@ FreeSwitch ESL implementation for Rust ```rust use freeswitch_esl::{Esl, EslError}; +use tokio::net::TcpStream; #[tokio::main] async fn main() -> Result<(), EslError> { let addr = "localhost:8021"; // Freeswitch host let password = "ClueCon"; - let inbound = Esl::inbound(addr, password).await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, password).await?; let reloadxml = inbound.api("reloadxml").await?; println!("reloadxml response : {:?}", reloadxml); @@ -33,6 +37,7 @@ async fn main() -> Result<(), EslError> { ## Outbound Example ```rust +use tokio::net::TcpListener; use freeswitch_esl::{Esl, EslConnection, EslError}; async fn process_call(conn: EslConnection) -> Result<(), EslError> { @@ -49,7 +54,6 @@ async fn process_call(conn: EslConnection) -> Result<(), EslError> { "conference/conf-bad-pin.wav", ) .await?; - println!("got digit {}", digit); conn.playback("ivr/ivr-you_entered.wav").await?; conn.playback(&format!("digits/{}.wav", digit)).await?; conn.hangup("NORMAL_CLEARING").await?; @@ -58,12 +62,13 @@ async fn process_call(conn: EslConnection) -> Result<(), EslError> { #[tokio::main] async fn main() -> Result<(), EslError> { - env_logger::init(); let addr = "0.0.0.0:8085"; // Listening address - let listener = Esl::outbound(addr).await?; + println!("Listening on {}", addr); + let listener = TcpListener::bind(addr).await?; loop { let (socket, _) = listener.accept().await?; + let socket = Esl::outbound(socket).await?; tokio::spawn(async move { process_call(socket).await }); } } diff --git a/examples/inbound.rs b/examples/inbound.rs index 50eed7b..733ed0b 100644 --- a/examples/inbound.rs +++ b/examples/inbound.rs @@ -1,10 +1,12 @@ use freeswitch_esl::{Esl, EslError}; +use tokio::net::TcpStream; #[tokio::main] async fn main() -> Result<(), EslError> { - let addr = "localhost:8021"; // Freeswitch host + let addr = "localhost:8022"; // Freeswitch host let password = "ClueCon"; - let inbound = Esl::inbound(addr, password).await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, password).await?; let reloadxml = inbound.api("reloadxml").await?; println!("reloadxml response : {:?}", reloadxml); diff --git a/examples/outbound.rs b/examples/outbound.rs index 92e51e8..427f121 100644 --- a/examples/outbound.rs +++ b/examples/outbound.rs @@ -1,3 +1,5 @@ +use tokio::net::TcpListener; + use freeswitch_esl::{Esl, EslConnection, EslError}; async fn process_call(conn: EslConnection) -> Result<(), EslError> { @@ -15,7 +17,7 @@ async fn process_call(conn: EslConnection) -> Result<(), EslError> { "conference/conf-bad-pin.wav", ) .await?; - println!("got digit {}", digit); + println!("Received Digit: {}", digit); conn.playback("ivr/ivr-you_entered.wav").await?; conn.playback(&format!("digits/{}.wav", digit)).await?; conn.hangup("NORMAL_CLEARING").await?; @@ -26,10 +28,11 @@ async fn process_call(conn: EslConnection) -> Result<(), EslError> { async fn main() -> Result<(), EslError> { let addr = "0.0.0.0:8085"; // Listening address println!("Listening on {}", addr); - let listener = Esl::outbound(addr).await?; + let listener = TcpListener::bind(addr).await?; loop { let (socket, _) = listener.accept().await?; + let socket = Esl::outbound(socket).await?; tokio::spawn(async move { process_call(socket).await }); } } diff --git a/src/code.rs b/src/code.rs deleted file mode 100644 index 5a5b49e..0000000 --- a/src/code.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum Code { - Ok, - Err, - Unknown, -} - -pub(crate) trait ParseCode { - fn parse_code(self) -> Result; -} -impl ParseCode for &str { - fn parse_code(self) -> Result { - match self { - "+OK" => Ok(Code::Ok), - "-ERR" => Ok(Code::Err), - _ => Ok(Code::Unknown), - } - } -} diff --git a/src/connection.rs b/src/connection.rs index 49bc8f1..8280ff8 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,32 +1,29 @@ -use crate::code::{Code, ParseCode}; use crate::error::EslError; use crate::esl::EslConnectionType; -use crate::event::Event; -use crate::io::EslCodec; -use futures::SinkExt; -use serde_json::Value; +use crate::parser::{ + parse_any_freeswitch_event, parse_auth_request, CommandAndApiReplyBody, FreeswitchReply, +}; +use dashmap::DashMap; use std::collections::{HashMap, VecDeque}; use std::sync::atomic::Ordering; use std::sync::{atomic::AtomicBool, Arc}; -use tokio::io::WriteHalf; -use tokio::net::{TcpStream, ToSocketAddrs}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::sync::mpsc; use tokio::sync::{ oneshot::{channel, Sender}, Mutex, }; -use tokio_stream::StreamExt; -use tokio_util::codec::{FramedRead, FramedWrite}; -use tracing::trace; +use tracing::{info, trace, warn}; #[derive(Debug)] /// contains Esl connection with freeswitch pub struct EslConnection { password: String, - commands: Arc>>>, - transport_tx: Arc, EslCodec>>>, - background_jobs: Arc>>>, + commands: Arc>>>, + command_channels: Arc>>>, + background_jobs: Arc>>, connected: AtomicBool, pub(crate) call_uuid: Option, - connection_info: Option>, + connection_info: HashMap, } impl EslConnection { @@ -45,102 +42,161 @@ impl EslConnection { self.connected.load(Ordering::Relaxed) } pub(crate) async fn send(&self, item: &[u8]) -> Result<(), EslError> { - let mut transport = self.transport_tx.lock().await; - transport.send(item).await + let sender = self.command_channels.lock().await; + let mut buffer = Vec::with_capacity(item.len() + 2); + buffer.extend_from_slice(item); + buffer.extend_from_slice(b"\n\n"); + let _ = sender.send(buffer).await; + Ok(()) } /// sends raw message to freeswitch and receives reply - pub async fn send_recv(&self, item: &[u8]) -> Result { + pub async fn send_recv(&self, item: &[u8]) -> Result { self.send(item).await?; let (tx, rx) = channel(); self.commands.lock().await.push_back(tx); Ok(rx.await?) } - pub(crate) async fn with_tcpstream( - stream: TcpStream, + pub(crate) async fn new( + mut stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static, password: impl ToString, connection_type: EslConnectionType, ) -> Result { - // let sender = Arc::new(sender); let commands = Arc::new(Mutex::new(VecDeque::new())); let inner_commands = Arc::clone(&commands); - let background_jobs = Arc::new(Mutex::new(HashMap::new())); + let background_jobs = Arc::new(DashMap::new()); let inner_background_jobs = Arc::clone(&background_jobs); - let esl_codec = EslCodec {}; - let (read_half, write_half) = tokio::io::split(stream); - let mut transport_rx = FramedRead::new(read_half, esl_codec.clone()); - let transport_tx = Arc::new(Mutex::new(FramedWrite::new(write_half, esl_codec.clone()))); + let mut dst = Vec::new(); + let mut bufs = [0; 1024]; if connection_type == EslConnectionType::Inbound { - transport_rx.next().await; + let bytes = stream.read(&mut bufs[..]).await.unwrap(); + let auth_message = String::from_utf8_lossy(&bufs[..bytes]); + let _ = parse_auth_request(&auth_message); } + let (mut read_half, mut write_half) = tokio::io::split(stream); + + let (tx, mut rx) = tokio::sync::mpsc::channel(100); let mut connection = Self { password: password.to_string(), commands, background_jobs, - transport_tx, + command_channels: Arc::new(Mutex::new(tx)), connected: AtomicBool::new(false), call_uuid: None, - connection_info: None, + connection_info: HashMap::default(), }; tokio::spawn(async move { + tokio::spawn(async move { + while let Some(data) = rx.recv().await { + let write = write_half.write(&data).await; + println!("writing result {:?}", write); + } + }); loop { - if let Some(Ok(event)) = transport_rx.next().await { - if let Some(event_type) = event.headers.get("Content-Type") { - match event_type.as_str().unwrap() { - "text/disconnect-notice" => { - trace!("got disconnect notice"); - return; + let read_bytes = read_half.read(&mut bufs[..]).await.unwrap(); + + if read_bytes == 0 { + break; + } + dst.extend_from_slice(&bufs[0..read_bytes]); + loop { + let inputs = String::from_utf8_lossy(&dst); + if let Ok(event) = parse_any_freeswitch_event(&inputs) { + let (remaining, parsed) = event; + dst.drain(0..inputs.len() - remaining.len()); + match parsed { + FreeswitchReply::AuthRequest => { + if let Some(tx) = inner_commands.lock().await.pop_front() { + tx.send(CommandAndApiReplyBody::default()).expect("msg"); + } } - "text/event-json" => { - trace!("got event-json"); - let data = event - .body() - .clone() - .expect("Unable to get body of event-json"); - let event_body = parse_json_body(&data) - .expect("Unable to parse body of event-json"); - let job_uuid = event_body.get("Job-UUID"); - if let Some(job_uuid) = job_uuid { - let job_uuid = job_uuid.as_str().unwrap(); - if let Some(tx) = - inner_background_jobs.lock().await.remove(job_uuid) - { - tx.send(event) - .expect("Unable to send channel message from bgapi"); - } - trace!("continued"); - continue; + FreeswitchReply::CommandAndApiReply(n) => { + if let Some(tx) = inner_commands.lock().await.pop_front() { + tx.send(n).expect("msg"); } - if let Some(application_uuid) = event_body.get("Application-UUID") { - let job_uuid = application_uuid.as_str().unwrap(); - if let Some(event_name) = event_body.get("Event-Name") { - if let Some(event_name) = event_name.as_str() { - if event_name == "CHANNEL_EXECUTE_COMPLETE" { - if let Some(tx) = inner_background_jobs - .lock() - .await - .remove(job_uuid) - { - tx.send(event).expect( - "Unable to send channel message from bgapi", + } + FreeswitchReply::Event(n) => { + if let (Some(job_uuid), Some(event_name)) = + (n.headers.get("Job-UUID"), n.headers.get("Event-Name")) + { + if event_name == "BACKGROUND_JOB" { + if let Some(tx) = inner_background_jobs.remove(job_uuid) { + match tx.1.send(FreeswitchReply::Event(n.clone())) { + Ok(_) => {} + Err(e) => { + warn!( + "error notifying background jobs {:?}", + e ); } - trace!("continued"); - trace!("got channel execute complete"); } + } else { + warn!( + "this is background job was not present {:?}", + job_uuid + ); + } + } + } + if let Some(event_name) = n.headers.get("Event-Name") { + if event_name == "CHANNEL_EXECUTE_COMPLETE" { + if let Some(application_uuid) = + n.headers.get("Application-UUID") + { + if let Some(tx) = + inner_background_jobs.remove(application_uuid) + { + match tx.1.send( + FreeswitchReply::CommandAndApiReply( + CommandAndApiReplyBody { + headers: n.headers.clone(), + code: n.code.clone(), + reply_text: n.body.clone(), + job_uuid: None, + }, + ), + ) { + Ok(_) => {} + Err(e) => { + warn!( + "error notifying background jobs {:?}", + e + ); + } + } + } else { + warn!( + "this is background job was not present {:?}", + application_uuid + ); + } + } + } + if event_name == "CHANNEL_DATA" { + info!("content-type {:?}", n.headers.get("Content-Type")); + if let Some(content_type) = n.headers.get("Content-Type") { + if content_type == "command/reply" { + if let Some(tx) = + inner_commands.lock().await.pop_front() + { + tx.send(CommandAndApiReplyBody { + headers: n.headers, + ..Default::default() + }) + .expect("msg"); + } + }; } } } - continue; } _ => { - trace!("got another event {:?}", event); + panic!("handle this case bro") } } - } - if let Some(tx) = inner_commands.lock().await.pop_front() { - tx.send(event).expect("msg"); + } else { + break; } } } @@ -155,21 +211,19 @@ impl EslConnection { } EslConnectionType::Outbound => { let response = connection.send_recv(b"connect").await?; - trace!("{:?}", response); - connection.connection_info = Some(response.headers().clone()); + connection.connection_info = response.headers.clone(); let response = connection .subscribe(vec!["BACKGROUND_JOB", "CHANNEL_EXECUTE_COMPLETE"]) .await?; trace!("{:?}", response); let response = connection.send_recv(b"myevents").await?; trace!("{:?}", response); - let connection_info = connection.connection_info.as_ref().unwrap(); - let channel_unique_id = connection_info + let channel_unique_id = connection + .connection_info .get("Channel-Unique-ID") .unwrap() - .as_str() - .unwrap(); + .as_str(); connection.call_uuid = Some(channel_unique_id.to_string()); } } @@ -177,65 +231,58 @@ impl EslConnection { } /// subscribes to given events - pub async fn subscribe(&self, events: Vec<&str>) -> Result { - let message = format!("event json {}", events.join(" ")); + pub async fn subscribe(&self, events: Vec<&str>) -> Result { + let message = format!("event plain {}", events.join(" ")); self.send_recv(message.as_bytes()).await } - pub(crate) async fn new( - socket: impl ToSocketAddrs, - password: impl ToString, - connection_type: EslConnectionType, - ) -> Result { - let stream = TcpStream::connect(socket).await?; - Self::with_tcpstream(stream, password, connection_type).await - } pub(crate) async fn auth(&self) -> Result { - let auth_response = self - .send_recv(format!("auth {}", self.password).as_bytes()) - .await?; - let auth_headers = auth_response.headers(); - let reply_text = auth_headers.get("Reply-Text").ok_or_else(|| { - EslError::InternalError("Reply-Text in auth request was not found".into()) - })?; - let reply_text = reply_text.as_str().unwrap(); - let (code, text) = parse_api_response(reply_text)?; - match code { - Code::Ok => { - self.connected.store(true, Ordering::Relaxed); - Ok(text) + { + let auth_response = self + .send_recv(format!("auth {}", self.password).as_bytes()) + .await?; + match auth_response.code { + crate::parser::Code::Ok => { + self.connected.store(true, Ordering::Relaxed); + Ok("AuthSuccess".into()) + } + crate::parser::Code::Err => { + self.connected.store(false, Ordering::Relaxed); + Err(EslError::AuthFailed) + } } - Code::Err => Err(EslError::AuthFailed), - Code::Unknown => Err(EslError::InternalError( - "Got unknown code in auth request".into(), - )), } } /// For hanging up call in outbound mode - pub async fn hangup(&self, reason: &str) -> Result { + pub async fn hangup(&self, reason: &str) -> Result { self.execute("hangup", reason).await } /// executes application in freeswitch - pub async fn execute(&self, app_name: &str, app_args: &str) -> Result { + pub async fn execute( + &self, + app_name: &str, + app_args: &str, + ) -> Result { let event_uuid = uuid::Uuid::new_v4().to_string(); let (tx, rx) = channel(); - self.background_jobs - .lock() - .await - .insert(event_uuid.clone(), tx); + self.background_jobs.insert(event_uuid.clone(), tx); let call_uuid = self.call_uuid.as_ref().unwrap().clone(); let command = format!("sendmsg {}\nexecute-app-name: {}\nexecute-app-arg: {}\ncall-command: execute\nEvent-UUID: {}",call_uuid,app_name,app_args,event_uuid); let response = self.send_recv(command.as_bytes()).await?; trace!("inside execute {:?}", response); let resp = rx.await?; - trace!("got response from channel {:?}", resp); - Ok(resp) + match resp { + FreeswitchReply::CommandAndApiReply(n) => Ok(n), + _ => { + panic!("this should not happened {:?}", resp); + } + } } /// answers call in outbound mode - pub async fn answer(&self) -> Result { + pub async fn answer(&self) -> Result { self.execute("answer", "").await } @@ -243,15 +290,9 @@ impl EslConnection { pub async fn api(&self, command: &str) -> Result { let response = self.send_recv(format!("api {}", command).as_bytes()).await; let event = response?; - let body = event - .body - .ok_or_else(|| EslError::InternalError("Didnt get body in api response".into()))?; - - let (code, text) = parse_api_response(&body)?; - match code { - Code::Ok => Ok(text), - Code::Err => Err(EslError::ApiError(text)), - Code::Unknown => Ok(body), + match event.code { + crate::parser::Code::Ok => Ok(event.reply_text), + crate::parser::Code::Err => Err(EslError::ApiError(event.reply_text)), } } @@ -260,51 +301,20 @@ impl EslConnection { trace!("Send bgapi {}", command); let job_uuid = uuid::Uuid::new_v4().to_string(); let (tx, rx) = channel(); - self.background_jobs - .lock() - .await - .insert(job_uuid.clone(), tx); + self.background_jobs.insert(job_uuid.clone(), tx); self.send_recv(format!("bgapi {}\nJob-UUID: {}", command, job_uuid).as_bytes()) .await?; let resp = rx.await?; - let body = resp - .body() - .clone() - .ok_or_else(|| EslError::InternalError("body was not found in event/json".into()))?; - - let body_hashmap = parse_json_body(&body)?; - - let mut hsmp = resp.headers().clone(); - hsmp.extend(body_hashmap); - let body = hsmp - .get("_body") - .ok_or_else(|| EslError::InternalError("body was not found in event/json".into()))?; - let body = body.as_str().unwrap(); - let (code, text) = parse_api_response(body)?; - match code { - Code::Ok => Ok(text), - Code::Err => Err(EslError::ApiError(text)), - Code::Unknown => Ok(body.to_string()), + match resp { + FreeswitchReply::Event(n) => match n.code { + crate::parser::Code::Ok => Ok(n.body), + crate::parser::Code::Err => Err(EslError::ApiError(n.body)), + }, + _ => { + panic!("unwanted data") + } } } } -fn parse_api_response(body: &str) -> Result<(Code, String), EslError> { - let space_index = body - .find(char::is_whitespace) - .ok_or_else(|| EslError::InternalError("Unable to find space index".into()))?; - let code = &body[..space_index]; - let text_start = space_index + 1; - let body_length = body.len(); - let text = if text_start < body_length { - body[text_start..(body_length - 1)].to_string() - } else { - String::new() - }; - let code = code.parse_code()?; - Ok((code, text)) -} -fn parse_json_body(body: &str) -> Result, EslError> { - Ok(serde_json::from_str(body)?) -} diff --git a/src/dp_tools.rs b/src/dp_tools.rs index 06afbcd..28ebd7b 100644 --- a/src/dp_tools.rs +++ b/src/dp_tools.rs @@ -1,46 +1,56 @@ -use std::collections::HashMap; - -use serde_json::Value; - const PLAY_AND_GET_DIGITS_APP: &str = "play_and_get_digits"; const PLAYBACK_APP: &str = "playback"; -use crate::{EslConnection, EslError, Event}; +use crate::{CommandAndApiReplyBody, EslConnection, EslError}; impl EslConnection { /// plays file in call during outbound mode - pub async fn playback(&self, file_path: &str) -> Result { + pub async fn playback(&self, file_path: &str) -> Result { self.execute(PLAYBACK_APP, file_path).await } /// record_session during outbound mode - pub async fn record_session(&self, file_path: &str) -> Result { + pub async fn record_session( + &self, + file_path: &str, + ) -> Result { self.execute("record_session", file_path).await } /// send dtmf during outbound mode - pub async fn send_dtmf(&self, dtmf_str: &str) -> Result { + pub async fn send_dtmf(&self, dtmf_str: &str) -> Result { self.execute("send_dtmf", dtmf_str).await } /// wait for silence during outbound mode - pub async fn wait_for_silence(&self, silence_str: &str) -> Result { + pub async fn wait_for_silence( + &self, + silence_str: &str, + ) -> Result { self.execute("wait_for_silence", silence_str).await } /// sleep for specified milliseconds in outbound mode - pub async fn sleep(&self, millis: u32) -> Result { + pub async fn sleep(&self, millis: u32) -> Result { self.execute("sleep", &millis.to_string()).await } ///set a channel variable - pub async fn set_variable(&self, var: &str, value: &str) -> Result { + pub async fn set_variable( + &self, + var: &str, + value: &str, + ) -> Result { let args = format!("{}={}", var, value); self.execute("set", &args).await } ///add a freeswitch log - pub async fn fs_log(&self, loglevel: &str, msg: &str) -> Result { + pub async fn fs_log( + &self, + loglevel: &str, + msg: &str, + ) -> Result { let args = format!("{} {}", loglevel, msg); self.execute("log", &args).await } @@ -62,17 +72,11 @@ impl EslConnection { "{min} {max} {tries} {timeout} {terminators} {file} {invalid_file} {variable_name}", ); let data = self.execute(PLAY_AND_GET_DIGITS_APP, &app_args).await?; - let body = data.body.as_ref().unwrap(); - let body = parse_json_body(body).unwrap(); + let body = &data.headers; let result = body.get(&format!("variable_{}", variable_name)); let Some(digit) = result else { return Err(EslError::NoInput); }; - let digit = digit.as_str().unwrap().to_string(); - Ok(digit) + Ok(digit.to_owned()) } } - -fn parse_json_body(body: &str) -> Result, EslError> { - Ok(serde_json::from_str(body)?) -} diff --git a/src/error.rs b/src/error.rs index 6a70c77..aadda14 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,7 @@ -use std::num::ParseIntError; - use thiserror::Error; +use tokio::sync::oneshot::error::RecvError; // Import std::io::Error as IoError -#[derive(Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Hash, Error)] +#[derive(Error, Debug, PartialEq)] #[allow(missing_docs)] /// Error type for Esl pub enum EslError { @@ -12,36 +11,19 @@ pub enum EslError { #[error("Wrong password.")] AuthFailed, - #[error("Unable to connect to destination server.")] + #[error("Unable to connect to destination server: {0}")] ConnectionError(String), #[error("{0:?}")] ApiError(String), - - #[error("")] - CodeParseError(), - #[error("Didnt get any digits")] NoInput, -} + #[error(transparent)] + ChannelError(#[from] RecvError), +} impl From for EslError { fn from(error: std::io::Error) -> Self { Self::InternalError(error.to_string()) } } -impl From for EslError { - fn from(error: tokio::sync::oneshot::error::RecvError) -> Self { - Self::InternalError(error.to_string()) - } -} -impl From for EslError { - fn from(error: serde_json::Error) -> Self { - Self::InternalError(error.to_string()) - } -} -impl From for EslError { - fn from(error: ParseIntError) -> Self { - Self::InternalError(error.to_string()) - } -} diff --git a/src/esl.rs b/src/esl.rs index 14728ce..0793cd5 100644 --- a/src/esl.rs +++ b/src/esl.rs @@ -1,6 +1,7 @@ -use tokio::net::ToSocketAddrs; +use tokio::io::{AsyncRead, AsyncWrite}; -use crate::{connection::EslConnection, outbound::Outbound, EslError}; +use crate::connection::EslConnection; +use crate::EslError; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum EslConnectionType { Inbound, @@ -11,14 +12,17 @@ pub struct Esl; impl Esl { /// Creates new inbound connection to freeswitch pub async fn inbound( - addr: impl ToSocketAddrs, + stream: impl AsyncRead + AsyncWrite + Send + 'static + Unpin, password: impl ToString, ) -> Result { - EslConnection::new(addr, password, EslConnectionType::Inbound).await + EslConnection::new(stream, password, EslConnectionType::Inbound).await } /// Creates new server for outbound connection - pub async fn outbound(addr: impl ToSocketAddrs) -> Result { - Outbound::bind(addr).await + pub async fn outbound( + stream: impl AsyncRead + AsyncWrite + Send + 'static + Unpin, + ) -> Result { + let connection = EslConnection::new(stream, "None", EslConnectionType::Outbound).await?; + Ok(connection) } } diff --git a/src/event.rs b/src/event.rs deleted file mode 100644 index 2b56c88..0000000 --- a/src/event.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::collections::HashMap; - -use serde_json::Value; - -#[derive(Debug, Clone, PartialEq, Eq)] -/// Structure of event returned from freeswitch -pub struct Event { - pub(crate) headers: HashMap, - pub(crate) body: Option, -} -impl Event { - /// Returns header from event - pub fn headers(&self) -> &HashMap { - &self.headers - } - /// Returns body from event - pub fn body(&self) -> &Option { - &self.body - } -} diff --git a/src/io.rs b/src/io.rs deleted file mode 100644 index 1451e54..0000000 --- a/src/io.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::collections::HashMap; - -use bytes::Buf; -use serde_json::Value; -use tokio_util::codec::{Decoder, Encoder}; -use tracing::{trace, warn}; - -use crate::{event::Event, EslError}; - -#[derive(Debug, Clone)] -pub(crate) struct EslCodec {} - -impl Encoder<&[u8]> for EslCodec { - type Error = EslError; - fn encode(&mut self, item: &[u8], dst: &mut bytes::BytesMut) -> Result<(), Self::Error> { - dst.extend_from_slice(item); - dst.extend_from_slice(b"\n\n"); - Ok(()) - } -} - -fn get_header_end(src: &bytes::BytesMut) -> Option { - trace!("get_header_end:=>{:?}", src); - // get first new line character - for (index, chat) in src[..].iter().enumerate() { - if chat == &b'\n' && src.get(index + 1) == Some(&b'\n') { - return Some(index + 1); - } - } - None -} -fn parse_body(src: &[u8], length: usize) -> String { - trace!("parse body src : {}", String::from_utf8_lossy(src)); - trace!("length src : {}", length); - String::from_utf8_lossy(&src[..length]).to_string() -} -fn parse_header(src: &[u8]) -> Result, std::io::Error> { - trace!("parsing this header {:#?}", String::from_utf8_lossy(src)); - let data = String::from_utf8_lossy(src).to_string(); - let a = data.split('\n'); - let mut hash = HashMap::new(); - for line in a { - let parts: Vec<&str> = line.split(':').collect(); - if parts.len() == 2 { - // SAFETY: Index access is safe beacue we have checked the length - let key = parts[0].trim(); - let val = parts[1].trim(); - hash.insert(key.to_string(), serde_json::json!(val.to_string())); - } else { - warn!("Invalid formatting while parsing header"); - } - } - trace!("returning hashmap : {:?}", hash); - Ok(hash) -} - -impl Decoder for EslCodec { - type Item = Event; - type Error = EslError; - fn decode(&mut self, src: &mut bytes::BytesMut) -> Result, Self::Error> { - trace!("decode"); - let header_end = get_header_end(src); - let header_end = match header_end { - Some(he) => he, - None => return Ok(None), - }; - let headers = parse_header(&src[..(header_end - 1)])?; - trace!("parsed headers are : {:?}", headers); - let body_start = header_end + 1; - let Some(length) = headers.get("Content-Length") else { - src.advance(body_start); - return Ok(Some(Event { - headers, - body: None, - })); - }; - - let length = length.as_str().unwrap(); - let body_length = length.parse()?; - if src.len() < (header_end + body_length + 1) { - trace!("returned because size was not enough"); - return Ok(None); - } - let body = parse_body(&src[body_start..], body_length); - src.advance(body_start + body_length); - Ok(Some(Event { - headers, - body: Some(body), - })) - } -} diff --git a/src/lib.rs b/src/lib.rs index d5b0646..b72726f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,12 +8,14 @@ //! //!```rust,no_run //! use freeswitch_esl::{Esl, EslError}; +//! use tokio::net::TcpStream; //! //! #[tokio::main] //! async fn main() -> Result<(), EslError> { //! let addr = "localhost:8021"; // Freeswitch host //! let password = "ClueCon"; -//! let inbound = Esl::inbound(addr, password).await?; +//! let stream = TcpStream::connect(addr).await?; +//! let inbound = Esl::inbound(stream, password).await?; //! //! let reloadxml = inbound.api("reloadxml").await?; //! println!("reloadxml response : {:?}", reloadxml); @@ -28,6 +30,7 @@ //! //!```rust,no_run //! use freeswitch_esl::{Esl, EslConnection, EslError}; +//! use tokio::net::TcpListener; //! //! async fn process_call(conn: EslConnection) -> Result<(), EslError> { //! conn.answer().await?; @@ -55,25 +58,23 @@ //! async fn main() -> Result<(), EslError> { //! let addr = "0.0.0.0:8085"; // Listening address //! println!("Listening on {}", addr); -//! let listener = Esl::outbound(addr).await?; +//! let listener = TcpListener::bind(addr).await?; //! //! loop { //! let (socket, _) = listener.accept().await?; +//! let socket = Esl::outbound(socket).await?; //! tokio::spawn(async move { process_call(socket).await }); //! } //! } //! ``` -pub(crate) mod code; pub(crate) mod connection; pub(crate) mod dp_tools; pub(crate) mod error; pub(crate) mod esl; -pub(crate) mod event; -pub(crate) mod io; -pub(crate) mod outbound; +pub(crate) mod parser; pub use connection::EslConnection; pub use error::*; pub use esl::*; -pub use event::*; +pub use parser::*; diff --git a/src/outbound.rs b/src/outbound.rs deleted file mode 100644 index d28ea7e..0000000 --- a/src/outbound.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::net::SocketAddr; - -use tokio::net::{TcpListener, ToSocketAddrs}; - -use crate::{connection::EslConnection, EslConnectionType, EslError}; - -pub struct Outbound { - listener: TcpListener, -} -impl Outbound { - pub(crate) async fn bind(addr: impl ToSocketAddrs) -> Result { - let listener = TcpListener::bind(addr).await?; - Ok(Self { listener }) - } - pub async fn accept(&self) -> Result<(EslConnection, SocketAddr), EslError> { - let (stream, addr) = self.listener.accept().await?; - let connection = - EslConnection::with_tcpstream(stream, "None", EslConnectionType::Outbound).await?; - Ok((connection, addr)) - } -} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..2417e46 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,485 @@ +#![allow(dead_code, missing_docs)] + +use std::collections::HashMap; + +use nom::{ + branch::alt, + bytes::complete::{tag, take, take_till, take_until}, + character::complete::{digit1, multispace0, newline, space0, space1}, + combinator::{map_res, opt, rest}, + multi::fold_many1, + sequence::pair, + IResult, +}; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum Errors { + #[error("Command failed")] + CommandFailed, +} +#[derive(Debug, Clone, Default, PartialEq)] +pub struct CommandAndApiReplyBody { + pub headers: HashMap, + pub code: Code, + pub reply_text: String, + pub job_uuid: Option, +} +#[derive(Debug, Clone, Default, PartialEq)] +pub enum Code { + #[default] + Ok, + Err, +} +#[derive(Debug, Clone, Default, PartialEq)] +pub struct BackgroundEvent { + pub code: Code, + pub body: String, + pub headers: HashMap, +} +#[derive(Debug, Clone, PartialEq)] +pub enum FreeswitchReply { + AuthRequest, + CommandAndApiReply(CommandAndApiReplyBody), + DisconnectNotice(String), + Event(BackgroundEvent), +} + +pub fn two_newlines(input: &str) -> IResult<&str, ()> { + let (input, _) = newline(input)?; + let (input, _) = newline(input)?; + Ok((input, ())) +} +pub fn parse_auth_request(input: &str) -> IResult<&str, crate::parser::FreeswitchReply> { + let (input, _) = tag("Content-Type: auth/request\n\n")(input)?; + Ok((input, FreeswitchReply::AuthRequest)) +} +pub fn parse_ok(input: &str) -> IResult<&str, Code> { + let (input, _) = tag("+OK")(input)?; + Ok((input, Code::Ok)) +} +pub fn parse_err(input: &str) -> IResult<&str, Code> { + let (input, _) = tag("-ERR")(input)?; + Ok((input, Code::Err)) +} +pub fn parse_code(input: &str) -> IResult<&str, Code> { + alt((parse_ok, parse_err))(input) +} +fn parse_body(input: &str) -> IResult<&str, (Code, String)> { + let (mut input, code) = opt(parse_code)(input)?; + if code.is_some() { + (input, _) = space1(input)?; + } + let code = code.unwrap_or(Code::Ok); + let (input, body) = rest(input)?; + let body = body.trim_end().to_string(); + Ok((input, (code, body.to_string()))) +} +pub fn parse_command_reply_with_job_uuid( + input: &str, +) -> IResult<&str, crate::parser::FreeswitchReply> { + let (input, _) = tag("Content-Type: command/reply")(input)?; + let (input, _) = newline(input)?; + let (input, _) = tag("Reply-Text: ")(input)?; + let (input, code) = parse_code(input)?; + let (input, _) = space0(input)?; + let (input, reply_text) = take_until("\n")(input)?; + let (input, _) = newline(input)?; + let (input, _) = tag("Job-UUID: ")(input)?; + let (input, job_uuid) = take_till(|c| c == '\n')(input)?; + let (input, _) = two_newlines(input)?; + let reply = FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + headers: HashMap::default(), + code, + reply_text: reply_text.to_string(), + job_uuid: Some(job_uuid.to_string()), + }); + Ok((input, reply)) +} +pub fn parse_command_reply(input: &str) -> IResult<&str, crate::parser::FreeswitchReply> { + let (input, _) = tag("Content-Type: command/reply")(input)?; + let (input, _) = newline(input)?; + let (input, _) = tag("Reply-Text: ")(input)?; + let (input, code) = parse_code(input)?; + let (input, _) = space0(input)?; + let (input, reply_text) = take_till(|c| c == '\n')(input)?; + let (input, _) = two_newlines(input)?; + let reply = FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + headers: HashMap::default(), + code, + reply_text: reply_text.to_string(), + job_uuid: None, + }); + Ok((input, reply)) +} +pub fn parse_content_length(input: &str) -> IResult<&str, u32> { + map_res(digit1, |s: &str| s.parse::())(input) +} +pub fn parse_disconnect_event(input: &str) -> IResult<&str, crate::parser::FreeswitchReply> { + let (input, _) = tag("Content-Type: text/disconnect-notice")(input)?; + let (input, _) = newline(input)?; + let (input, _) = tag("Content-Length: ")(input)?; + let (input, content_length) = parse_content_length(input)?; + let (input, _) = two_newlines(input)?; + let (input, content) = take(content_length - 1)(input)?; + let (input, _) = two_newlines(input)?; + let reply = FreeswitchReply::DisconnectNotice(content.to_string()); + Ok((input, reply)) +} +pub fn parse_api_response(input: &str) -> IResult<&str, crate::parser::FreeswitchReply> { + let (input, _) = tag("Content-Type: api/response")(input)?; + let (input, _) = newline(input)?; + let (input, _) = tag("Content-Length: ")(input)?; + let (input, content_length) = parse_content_length(input)?; + let (input, _) = two_newlines(input)?; + let (input, content) = take(content_length)(input)?; + let (mut content, code) = opt(parse_code)(content)?; + if code.is_some() { + // Space is optional if body is only +OK + (content, _) = opt(space1)(content)?; + } + let (_, response) = rest(content)?; + let reply = FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + headers: HashMap::default(), + code: code.unwrap_or(Code::Ok), + reply_text: response.trim_end().into(), + job_uuid: None, + }); + Ok((input, reply)) +} + +pub fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { + let (input, key) = take_until(":")(input)?; + let (input, _) = pair(tag(":"), multispace0)(input)?; + let (input, value) = take_until("\n")(input)?; + let (input, _) = tag("\n")(input)?; + Ok((input, (key, value))) +} +pub fn parse_colon_seperated(input: &str) -> IResult<&str, HashMap> { + fold_many1(parse_key_value, HashMap::new, |mut map, (key, value)| { + map.insert(key.to_string(), value.to_string()); + map + })(input) +} + +pub fn parse_plain_event(input: &str) -> IResult<&str, crate::parser::FreeswitchReply> { + let (input, _) = tag("Content-Length: ")(input)?; + let (input, content_length) = parse_content_length(input)?; + let (input, _) = newline(input)?; + let (input, _) = tag("Content-Type: text/event-plain")(input)?; + let (input, _) = newline(input)?; + let (mut input, content) = take(content_length)(input)?; + let (content, _) = newline(content)?; + let (remaining, maps) = parse_colon_seperated(content)?; + let body = if let Some(length) = maps.get("Content-Length") { + let (_, optional_body) = parse_optional_body(length.parse().unwrap(), remaining)?; + (input, _) = tag("\n")(input)?; + optional_body + } else { + "" + }; + + let (_, (code, data)) = parse_body(body).unwrap(); + let reply = FreeswitchReply::Event(BackgroundEvent { + code, + body: data, + headers: maps.clone(), + }); + let (input, _) = opt(newline)(input)?; + Ok((input, reply)) +} + +fn parse_optional_body(content_length: usize, input: &str) -> IResult<&str, &str> { + let (input, body) = take(content_length)(input)?; + Ok((input, body.trim())) +} +fn parse_colon_seperated_reply(input: &str) -> IResult<&str, FreeswitchReply> { + let (input_modified, data) = parse_colon_seperated(input)?; + let (input_modified, _) = newline(input_modified)?; + let command_reply = "command/reply".to_string(); + if let Some(content_type) = data.get("Content-Type") { + if content_type == &command_reply { + return Ok(( + input_modified, + FreeswitchReply::Event(BackgroundEvent { + headers: data, + ..Default::default() + }), + )); + } + } + Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Eof, + ))) +} + +pub fn parse_any_freeswitch_event(input: &str) -> IResult<&str, crate::parser::FreeswitchReply> { + alt(( + parse_command_reply_with_job_uuid, + parse_command_reply, + parse_api_response, + parse_plain_event, + parse_disconnect_event, + parse_auth_request, + parse_colon_seperated_reply, + ))(input) +} +#[cfg(test)] +mod tests { + + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + #[test] + fn auth_request() { + let input = "Content-Type: auth/request\n\n"; + assert_eq!( + parse_auth_request(input), + Ok(("", FreeswitchReply::AuthRequest)) + ) + } + + #[test] + fn check_ok_code() { + let input = "+OK"; + assert_eq!(parse_ok(input), Ok(("", Code::Ok))) + } + #[test] + fn check_err_code() { + let input = "-ERR"; + assert_eq!(parse_err(input), Ok(("", Code::Err))) + } + #[test] + fn check_code_parser() { + let input = "-ERR"; + assert_eq!(parse_code(input), Ok(("", Code::Err))); + let input = "+OK"; + assert_eq!(parse_code(input), Ok(("", Code::Ok))); + } + + #[test] + fn parse_command_reply_1() { + let input = "Content-Type: command/reply\nReply-Text: +OK event listener enabled json\n\n"; + assert_eq!( + parse_command_reply(input), + Ok(( + "", + FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + headers: HashMap::default(), + code: Code::Ok, + reply_text: "event listener enabled json".to_string(), + job_uuid: None, + }) + )) + ) + } + #[test] + fn parse_command_reply_2() { + let input = "Content-Type: command/reply\nReply-Text: +OK Job-UUID: 0435d687-db9c-46b6-9221-79f82852c1a0\nJob-UUID: 0435d687-db9c-46b6-9221-79f82852c1a0\n\n"; + assert_eq!( + parse_command_reply_with_job_uuid(input), + Ok(( + "", + FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + headers: HashMap::default(), + code: Code::Ok, + reply_text: "Job-UUID: 0435d687-db9c-46b6-9221-79f82852c1a0".to_string(), + job_uuid: Some("0435d687-db9c-46b6-9221-79f82852c1a0".to_string()), + }) + )) + ) + } + #[test] + fn test_parsing_disconnect_notice() { + let input = "Content-Type: text/disconnect-notice\nContent-Length: 67\n\nDisconnected, goodbye.\nSee you at ClueCon! http://www.cluecon.com/\n\n"; + assert_eq!( + parse_disconnect_event(input), + Ok(( + "", + FreeswitchReply::DisconnectNotice( + "Disconnected, goodbye.\nSee you at ClueCon! http://www.cluecon.com/" + .to_string() + ) + )) + ) + } + #[test] + fn test_parsing_api_response_1() { + let input = "Content-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n"; + assert_eq!( + parse_api_response(input), + Ok(( + "", + FreeswitchReply::CommandAndApiReply({ + CommandAndApiReplyBody { + headers: HashMap::default(), + code: Code::Err, + reply_text: "SUBSCRIBER_ABSENT".into(), + job_uuid: None, + } + }) + )) + ) + } + #[test] + fn test_parsing_api_response_2() { + // let input = "Content-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n\n"; + let input = "Content-Type: api/response\nContent-Length: 14\n\n+OK [Success]\n"; + assert_eq!( + parse_api_response(input), + Ok(( + "", + FreeswitchReply::CommandAndApiReply({ + CommandAndApiReplyBody { + headers: HashMap::default(), + code: Code::Ok, + reply_text: "[Success]".into(), + job_uuid: None, + } + }) + )) + ) + } + #[test] + fn test_parsing_api_response_3() { + // let input = "Content-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n\n"; + let input = "Content-Type: api/response\nContent-Length: 41\n\nReload XML [Success]\nrestarting: external"; + assert_eq!( + parse_api_response(input), + Ok(( + "", + FreeswitchReply::CommandAndApiReply({ + CommandAndApiReplyBody { + headers: HashMap::default(), + code: Code::Ok, + reply_text: "Reload XML [Success]\nrestarting: external".into(), + job_uuid: None, + } + }) + )) + ) + } + #[test] + fn test_parsing_api_response_4() { + let input = "Content-Type: api/response\nContent-Length: 4\n\n+OK\n"; + assert_eq!( + parse_api_response(input), + Ok(( + "", + FreeswitchReply::CommandAndApiReply({ + CommandAndApiReplyBody { + code: Code::Ok, + reply_text: "".into(), + job_uuid: None, + ..Default::default() + } + }) + )) + ) + } + + #[test] + fn test_parsing_body() { + let input = "+OK [Success]\n"; + assert_eq!(parse_body(input), Ok(("", (Code::Ok, "[Success]".into())))); + } + #[test] + fn test_parsing_multiple_request() { + let input = "Content-Type: api/response\nContent-Length: 14\n\n+OK [Success]\nContent-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n"; + let (input, event) = parse_any_freeswitch_event(input).unwrap(); + assert_eq!( + input, + "Content-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n" + ); + assert_eq!( + event, + (FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + code: Code::Ok, + reply_text: "[Success]".into(), + job_uuid: None, + ..Default::default() + })) + ); + let (input, event) = parse_any_freeswitch_event(input).unwrap(); + assert_eq!(input, ""); + assert_eq!( + event, + (FreeswitchReply::CommandAndApiReply(CommandAndApiReplyBody { + code: Code::Err, + reply_text: "SUBSCRIBER_ABSENT".into(), + job_uuid: None, + ..Default::default() + })) + ); + } + + #[test] + fn test_parsing_single_key_pair() { + let input = "Event-Name: CHANNEL_EXECUTE_COMPLETE\n"; + let (input, (key, value)) = parse_key_value(input).unwrap(); + assert_eq!(input, ""); + assert_eq!(key, "Event-Name"); + assert_eq!(value, "CHANNEL_EXECUTE_COMPLETE"); + } + #[test] + fn test_parsing_multiple_key_pair() { + let input = "Event-Name: CHANNEL_EXECUTE_COMPLETE\nCore-UUID: bd0e8916-6a60-4e11-8978-db8580b440a6\nFreeSWITCH-Hostname: ip-172-31-32-63\n"; + let (input, result) = parse_colon_seperated(input).unwrap(); + assert_eq!(input, ""); + assert_eq!( + result.get("Event-Name"), + Some(&"CHANNEL_EXECUTE_COMPLETE".to_owned()) + ); + assert_eq!( + result.get("Core-UUID"), + Some(&"bd0e8916-6a60-4e11-8978-db8580b440a6".to_owned()) + ); + assert_eq!( + result.get("FreeSWITCH-Hostname"), + Some(&"ip-172-31-32-63".to_owned()) + ); + } + #[test] + fn test_parsing_with_plain_event_1() { + let input = "Content-Length: 2763\nContent-Type: text/event-plain\n\nEvent-Name: CHANNEL_EXECUTE_COMPLETE\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2008%3A58%3A59\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2008%3A58%3A59%20GMT\nEvent-Date-Timestamp: 1695545939666460\nEvent-Calling-File: switch_core_session.c\nEvent-Calling-Function: switch_core_session_exec\nEvent-Calling-Line-Number: 2967\nEvent-Sequence: 5442\nChannel-State: CS_EXECUTE\nChannel-Call-State: RINGING\nChannel-State-Number: 4\nChannel-Name: loopback/1000-b\nUnique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCall-Direction: inbound\nPresence-Call-Direction: inbound\nChannel-HIT-Dialplan: true\nChannel-Call-UUID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nAnswer-State: ringing\nChannel-Read-Codec-Name: L16\nChannel-Read-Codec-Rate: 8000\nChannel-Read-Codec-Bit-Rate: 128000\nChannel-Write-Codec-Name: L16\nChannel-Write-Codec-Rate: 8000\nChannel-Write-Codec-Bit-Rate: 128000\nCaller-Direction: inbound\nCaller-Logical-Direction: inbound\nCaller-Dialplan: xml\nCaller-Caller-ID-Number: 0000000000\nCaller-Orig-Caller-ID-Number: 0000000000\nCaller-Callee-ID-Name: Outbound%20Call\nCaller-Callee-ID-Number: 1000\nCaller-ANI: 0000000000\nCaller-Destination-Number: 1000\nCaller-Unique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCaller-Source: mod_loopback\nCaller-Context: default\nCaller-Channel-Name: loopback/1000-b\nCaller-Profile-Index: 1\nCaller-Profile-Created-Time: 1695545939666460\nCaller-Channel-Created-Time: 1695545939666460\nCaller-Channel-Answered-Time: 0\nCaller-Channel-Progress-Time: 0\nCaller-Channel-Progress-Media-Time: 0\nCaller-Channel-Hangup-Time: 0\nCaller-Channel-Transfer-Time: 0\nCaller-Channel-Resurrect-Time: 0\nCaller-Channel-Bridged-Time: 0\nCaller-Channel-Last-Hold: 0\nCaller-Channel-Hold-Accum: 0\nCaller-Screen-Bit: true\nCaller-Privacy-Hide-Name: false\nCaller-Privacy-Hide-Number: false\nvariable_direction: inbound\nvariable_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_session_id: 58\nvariable_channel_name: loopback/1000-b\nvariable_read_codec: L16\nvariable_read_rate: 8000\nvariable_write_codec: L16\nvariable_write_rate: 8000\nvariable_origination_uuid: karan\nvariable_other_loopback_leg_uuid: karan\nvariable_loopback_leg: B\nvariable_DP_MATCH: ARRAY%3A%3A1000%7C%3A1000\nvariable_call_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_dialed_extension: 1000\nvariable_export_vars: dialed_extension\nvariable_current_application_data: 1%20b%20s%20execute_extension%3A%3Adx%20XML%20features\nvariable_current_application: bind_meta_app\nApplication: bind_meta_app\nApplication-Data: 1%20b%20s%20execute_extension%3A%3Adx%20XML%20features\nApplication-Response: _none_\nApplication-UUID: 55ec8e17-a7a3-44de-812b-ca08e6ec07a7\n"; + let (input, data) = parse_plain_event(input).unwrap(); + assert_eq!(input, ""); + match data { + FreeswitchReply::Event(n) => { + let event_name = n.headers.get("Event-Name"); + assert_eq!(event_name, Some(&"CHANNEL_EXECUTE_COMPLETE".to_string())); + let body_data = n.body; + assert_eq!(body_data, ""); + } + _ => panic!("Should not happen"), + } + } + #[test] + fn test_parsing_with_plain_event_background_job() { + let input = "Content-Length: 575\nContent-Type: text/event-plain\n\nEvent-Name: BACKGROUND_JOB\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2005%3A48%3A28\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2005%3A48%3A28%20GMT\nEvent-Date-Timestamp: 1695534508726403\nEvent-Calling-File: mod_event_socket.c\nEvent-Calling-Function: api_exec\nEvent-Calling-Line-Number: 1572\nEvent-Sequence: 1041\nJob-UUID: dcab6b81-ec71-4552-b897-88721870fe16\nJob-Command: reloadxml\nContent-Length: 14\n\n+OK [Success]\n"; + let (input, data) = parse_plain_event(input).unwrap(); + assert_eq!(input, ""); + match data { + FreeswitchReply::Event(n) => { + let event_name = n.headers.get("Event-Name"); + assert_eq!(event_name, Some(&"BACKGROUND_JOB".to_string())); + let body_data = n.body; + assert_eq!(body_data, "[Success]"); + } + _ => panic!("Should not happen"), + } + } + #[test] + fn test_parsing_random_event() { + let input = "Content-Length: 2687\nContent-Type: text/event-plain\n\nEvent-Name: CHANNEL_EXECUTE_COMPLETE\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2008%3A58%3A59\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2008%3A58%3A59%20GMT\nEvent-Date-Timestamp: 1695545939666460\nEvent-Calling-File: switch_core_session.c\nEvent-Calling-Function: switch_core_session_exec\nEvent-Calling-Line-Number: 2967\nEvent-Sequence: 5440\nChannel-State: CS_EXECUTE\nChannel-Call-State: RINGING\nChannel-State-Number: 4\nChannel-Name: loopback/1000-b\nUnique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCall-Direction: inbound\nPresence-Call-Direction: inbound\nChannel-HIT-Dialplan: true\nChannel-Call-UUID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nAnswer-State: ringing\nChannel-Read-Codec-Name: L16\nChannel-Read-Codec-Rate: 8000\nChannel-Read-Codec-Bit-Rate: 128000\nChannel-Write-Codec-Name: L16\nChannel-Write-Codec-Rate: 8000\nChannel-Write-Codec-Bit-Rate: 128000\nCaller-Direction: inbound\nCaller-Logical-Direction: inbound\nCaller-Dialplan: xml\nCaller-Caller-ID-Number: 0000000000\nCaller-Orig-Caller-ID-Number: 0000000000\nCaller-Callee-ID-Name: Outbound%20Call\nCaller-Callee-ID-Number: 1000\nCaller-ANI: 0000000000\nCaller-Destination-Number: 1000\nCaller-Unique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCaller-Source: mod_loopback\nCaller-Context: default\nCaller-Channel-Name: loopback/1000-b\nCaller-Profile-Index: 1\nCaller-Profile-Created-Time: 1695545939666460\nCaller-Channel-Created-Time: 1695545939666460\nCaller-Channel-Answered-Time: 0\nCaller-Channel-Progress-Time: 0\nCaller-Channel-Progress-Media-Time: 0\nCaller-Channel-Hangup-Time: 0\nCaller-Channel-Transfer-Time: 0\nCaller-Channel-Resurrect-Time: 0\nCaller-Channel-Bridged-Time: 0\nCaller-Channel-Last-Hold: 0\nCaller-Channel-Hold-Accum: 0\nCaller-Screen-Bit: true\nCaller-Privacy-Hide-Name: false\nCaller-Privacy-Hide-Number: false\nvariable_direction: inbound\nvariable_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_session_id: 58\nvariable_channel_name: loopback/1000-b\nvariable_read_codec: L16\nvariable_read_rate: 8000\nvariable_write_codec: L16\nvariable_write_rate: 8000\nvariable_origination_uuid: karan\nvariable_other_loopback_leg_uuid: karan\nvariable_loopback_leg: B\nvariable_DP_MATCH: ARRAY%3A%3A1000%7C%3A1000\nvariable_call_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_current_application_data: dialed_extension%3D1000\nvariable_current_application: export\nvariable_dialed_extension: 1000\nvariable_export_vars: dialed_extension\nApplication: export\nApplication-Data: dialed_extension%3D1000\nApplication-Response: _none_\nApplication-UUID: 9c016dd8-b8e6-4e64-9034-7aa0ffbf69e7\n\nContent-Length: 2763\nContent-Type: text/event-plain\n\nEvent-Name: CHANNEL_EXECUTE_COMPLETE\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2008%3A58%3A59\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2008%3A58%3A59%20GMT\nEvent-Date-Timestamp: 1695545939666460\nEvent-Calling-File: switch_core_session.c\nEvent-Calling-Function: switch_core_session_exec\nEvent-Calling-Line-Number: 2967\nEvent-Sequence: 5442\nChannel-State: CS_EXECUTE\nChannel-Call-State: RINGING\nChannel-State-Number: 4\nChannel-Name: loopback/1000-b\nUnique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCall-Direction: inbound\nPresence-Call-Direction: inbound\nChannel-HIT-Dialplan: true\nChannel-Call-UUID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nAnswer-State: ringing\nChannel-Read-Codec-Name: L16\nChannel-Read-Codec-Rate: 8000\nChannel-Read-Codec-Bit-Rate: 128000\nChannel-Write-Codec-Name: L16\nChannel-Write-Codec-Rate: 8000\nChannel-Write-Codec-Bit-Rate: 128000\nCaller-Direction: inbound\nCaller-Logical-Direction: inbound\nCaller-Dialplan: xml\nCaller-Caller-ID-Number: 0000000000\nCaller-Orig-Caller-ID-Number: 0000000000\nCaller-Callee-ID-Name: Outbound%20Call\nCaller-Callee-ID-Number: 1000\nCaller-ANI: 0000000000\nCaller-Destination-Number: 1000\nCaller-Unique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCaller-Source: mod_loopback\nCaller-Context: default\nCaller-Channel-Name: loopback/1000-b\nCaller-Profile-Index: 1\nCaller-Profile-Created-Time: 1695545939666460\nCaller-Channel-Created-Time: 1695545939666460\nCaller-Channel-Answered-Time: 0\nCaller-Channel-Progress-Time: 0\nCaller-Channel-Progress-Media-Time: 0\nCaller-Channel-Hangup-Time: 0\nCaller-Channel-Transfer-Time: 0\nCaller-Channel-Resurrect-Time: 0\nCaller-Channel-Bridged-Time: 0\nCaller-Channel-Last-Hold: 0\nCaller-Channel-Hold-Accum: 0\nCaller-Screen-Bit: true\nCaller-Privacy-Hide-Name: false\nCaller-Privacy-Hide-Number: false\nvariable_direction: inbound\nvariable_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_session_id: 58\nvariable_channel_name: loopback/1000-b\nvariable_read_codec: L16\nvariable_read_rate: 8000\nvariable_write_codec: L16\nvariable_write_rate: 8000\nvariable_origination_uuid: karan\nvariable_other_loopback_leg_uuid: karan\nvariable_loopback_leg: B\nvariable_DP_MATCH: ARRAY%3A%3A1000%7C%3A1000\nvariable_call_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_dialed_extension: 1000\nvariable_export_vars: dialed_extension\nvariable_current_application_data: 1%20b%20s%20execute_extension%3A%3Adx%20XML%20features\nvariable_current_application: bind_meta_app\nApplication: bind_meta_app\nApplication-Data: 1%20b%20s%20execute_extension%3A%3Adx%20XML%20features\nApplication-Response: _none_\nApplication-UUID: 55ec8e17-a7a3-44de-812b-ca08e6ec07a7\n"; + let remaining_input = "Content-Length: 2763\nContent-Type: text/event-plain\n\nEvent-Name: CHANNEL_EXECUTE_COMPLETE\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2008%3A58%3A59\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2008%3A58%3A59%20GMT\nEvent-Date-Timestamp: 1695545939666460\nEvent-Calling-File: switch_core_session.c\nEvent-Calling-Function: switch_core_session_exec\nEvent-Calling-Line-Number: 2967\nEvent-Sequence: 5442\nChannel-State: CS_EXECUTE\nChannel-Call-State: RINGING\nChannel-State-Number: 4\nChannel-Name: loopback/1000-b\nUnique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCall-Direction: inbound\nPresence-Call-Direction: inbound\nChannel-HIT-Dialplan: true\nChannel-Call-UUID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nAnswer-State: ringing\nChannel-Read-Codec-Name: L16\nChannel-Read-Codec-Rate: 8000\nChannel-Read-Codec-Bit-Rate: 128000\nChannel-Write-Codec-Name: L16\nChannel-Write-Codec-Rate: 8000\nChannel-Write-Codec-Bit-Rate: 128000\nCaller-Direction: inbound\nCaller-Logical-Direction: inbound\nCaller-Dialplan: xml\nCaller-Caller-ID-Number: 0000000000\nCaller-Orig-Caller-ID-Number: 0000000000\nCaller-Callee-ID-Name: Outbound%20Call\nCaller-Callee-ID-Number: 1000\nCaller-ANI: 0000000000\nCaller-Destination-Number: 1000\nCaller-Unique-ID: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nCaller-Source: mod_loopback\nCaller-Context: default\nCaller-Channel-Name: loopback/1000-b\nCaller-Profile-Index: 1\nCaller-Profile-Created-Time: 1695545939666460\nCaller-Channel-Created-Time: 1695545939666460\nCaller-Channel-Answered-Time: 0\nCaller-Channel-Progress-Time: 0\nCaller-Channel-Progress-Media-Time: 0\nCaller-Channel-Hangup-Time: 0\nCaller-Channel-Transfer-Time: 0\nCaller-Channel-Resurrect-Time: 0\nCaller-Channel-Bridged-Time: 0\nCaller-Channel-Last-Hold: 0\nCaller-Channel-Hold-Accum: 0\nCaller-Screen-Bit: true\nCaller-Privacy-Hide-Name: false\nCaller-Privacy-Hide-Number: false\nvariable_direction: inbound\nvariable_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_session_id: 58\nvariable_channel_name: loopback/1000-b\nvariable_read_codec: L16\nvariable_read_rate: 8000\nvariable_write_codec: L16\nvariable_write_rate: 8000\nvariable_origination_uuid: karan\nvariable_other_loopback_leg_uuid: karan\nvariable_loopback_leg: B\nvariable_DP_MATCH: ARRAY%3A%3A1000%7C%3A1000\nvariable_call_uuid: 81bdc2ed-2be3-42a8-93dd-ab596f352c83\nvariable_dialed_extension: 1000\nvariable_export_vars: dialed_extension\nvariable_current_application_data: 1%20b%20s%20execute_extension%3A%3Adx%20XML%20features\nvariable_current_application: bind_meta_app\nApplication: bind_meta_app\nApplication-Data: 1%20b%20s%20execute_extension%3A%3Adx%20XML%20features\nApplication-Response: _none_\nApplication-UUID: 55ec8e17-a7a3-44de-812b-ca08e6ec07a7\n"; + let (input, _) = parse_any_freeswitch_event(input).unwrap(); + assert_eq!(input, remaining_input); + } + + #[test] + fn test_parsing_outbound_connect_event() { + let input ="Event-Name: CHANNEL_DATA\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2010%3A04%3A06\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2010%3A04%3A06%20GMT\nEvent-Date-Timestamp: 1695549846366420\nEvent-Calling-File: mod_event_socket.c\nEvent-Calling-Function: parse_command\nEvent-Calling-Line-Number: 2021\nEvent-Sequence: 7934\nChannel-Direction: inbound\nChannel-Logical-Direction: inbound\nChannel-Username: 1000\nChannel-Dialplan: XML\nChannel-Caller-ID-Name: 1000\nChannel-Caller-ID-Number: 1000\nChannel-Orig-Caller-ID-Name: 1000\nChannel-Orig-Caller-ID-Number: 1000\nChannel-Network-Addr: 122.172.98.23\nChannel-ANI: 1000\nChannel-Destination-Number: 9999\nChannel-Unique-ID: c8130d8b-2109-47eb-bdbc-a8f7159f2821\nChannel-Source: mod_sofia\nChannel-Context: default\nChannel-Channel-Name: sofia/internal/1000%403.110.42.145\nChannel-Profile-Index: 1\nChannel-Profile-Created-Time: 1695549658226424\nChannel-Channel-Created-Time: 1695549658226424\nChannel-Channel-Answered-Time: 0\nChannel-Channel-Progress-Time: 0\nChannel-Channel-Progress-Media-Time: 0\nChannel-Channel-Hangup-Time: 0\nChannel-Channel-Transfer-Time: 0\nChannel-Channel-Resurrect-Time: 0\nChannel-Channel-Bridged-Time: 0\nChannel-Channel-Last-Hold: 0\nChannel-Channel-Hold-Accum: 0\nChannel-Screen-Bit: true\nChannel-Privacy-Hide-Name: false\nChannel-Privacy-Hide-Number: false\nChannel-State: CS_EXECUTE\nChannel-Call-State: RINGING\nChannel-State-Number: 4\nChannel-Name: sofia/internal/1000%403.110.42.145\nUnique-ID: c8130d8b-2109-47eb-bdbc-a8f7159f2821\nCall-Direction: inbound\nPresence-Call-Direction: inbound\nChannel-HIT-Dialplan: true\nChannel-Presence-ID: 1000%403.110.42.145\nChannel-Call-UUID: c8130d8b-2109-47eb-bdbc-a8f7159f2821\nAnswer-State: ringing\nCaller-Direction: inbound\nCaller-Logical-Direction: inbound\nCaller-Username: 1000\nCaller-Dialplan: XML\nCaller-Caller-ID-Name: 1000\nCaller-Caller-ID-Number: 1000\nCaller-Orig-Caller-ID-Name: 1000\nCaller-Orig-Caller-ID-Number: 1000\nCaller-Network-Addr: 122.172.98.23\nCaller-ANI: 1000\nCaller-Destination-Number: 9999\nCaller-Unique-ID: c8130d8b-2109-47eb-bdbc-a8f7159f2821\nCaller-Source: mod_sofia\nCaller-Context: default\nCaller-Channel-Name: sofia/internal/1000%403.110.42.145\nCaller-Profile-Index: 1\nCaller-Profile-Created-Time: 1695549658226424\nCaller-Channel-Created-Time: 1695549658226424\nCaller-Channel-Answered-Time: 0\nCaller-Channel-Progress-Time: 0\nCaller-Channel-Progress-Media-Time: 0\nCaller-Channel-Hangup-Time: 0\nCaller-Channel-Transfer-Time: 0\nCaller-Channel-Resurrect-Time: 0\nCaller-Channel-Bridged-Time: 0\nCaller-Channel-Last-Hold: 0\nCaller-Channel-Hold-Accum: 0\nCaller-Screen-Bit: true\nCaller-Privacy-Hide-Name: false\nCaller-Privacy-Hide-Number: false\nvariable_direction: inbound\nvariable_uuid: c8130d8b-2109-47eb-bdbc-a8f7159f2821\nvariable_session_id: 97\nvariable_sip_from_user: 1000\nvariable_sip_from_uri: 1000%403.110.42.145\nvariable_sip_from_host: 3.110.42.145\nvariable_video_media_flow: disabled\nvariable_audio_media_flow: disabled\nvariable_text_media_flow: disabled\nvariable_channel_name: sofia/internal/1000%403.110.42.145\nvariable_sip_call_id: d38a73d0e3ac414d9f0893915597aa6b\nvariable_sip_local_network_addr: 3.110.42.145\nvariable_sip_network_ip: 122.172.98.23\nvariable_sip_network_port: 50456\nvariable_sip_invite_stamp: 1695549658226424\nvariable_sip_received_ip: 122.172.98.23\nvariable_sip_received_port: 50456\nvariable_sip_via_protocol: udp\nvariable_sip_authorized: true\nvariable_Event-Name: REQUEST_PARAMS\nvariable_Core-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nvariable_FreeSWITCH-Hostname: ip-172-31-5-95\nvariable_FreeSWITCH-Switchname: ip-172-31-5-95\nvariable_FreeSWITCH-IPv4: 172.31.5.95\nvariable_FreeSWITCH-IPv6: %3A%3A1\nvariable_Event-Date-Local: 2023-09-24%2010%3A00%3A58\nvariable_Event-Date-GMT: Sun,%2024%20Sep%202023%2010%3A00%3A58%20GMT\nvariable_Event-Date-Timestamp: 1695549658226424\nvariable_Event-Calling-File: sofia.c\nvariable_Event-Calling-Function: sofia_handle_sip_i_invite\nvariable_Event-Calling-Line-Number: 10722\nvariable_Event-Sequence: 7881\nvariable_sip_number_alias: 1000\nvariable_sip_auth_username: 1000\nvariable_sip_auth_realm: 3.110.42.145\nvariable_number_alias: 1000\nvariable_requested_user_name: 1000\nvariable_requested_domain_name: 172.31.5.95\nvariable_record_stereo: true\nvariable_default_gateway: example.com\nvariable_default_areacode: 918\nvariable_transfer_fallback_extension: operator\nvariable_toll_allow: domestic,international,local\nvariable_accountcode: 1000\nvariable_user_context: default\nvariable_effective_caller_id_name: Extension%201000\nvariable_effective_caller_id_number: 1000\nvariable_outbound_caller_id_name: FreeSWITCH\nvariable_outbound_caller_id_number: 0000000000\nvariable_callgroup: techsupport\nvariable_user_name: 1000\nvariable_domain_name: 172.31.5.95\nvariable_sip_from_user_stripped: 1000\nvariable_sip_from_tag: d87f0933f993479f824c7387e619bbf4\nvariable_sofia_profile_name: internal\nvariable_sofia_profile_url: sip%3Amod_sofia%403.110.42.145%3A5060\nvariable_recovery_profile_name: internal\nvariable_sip_full_via: SIP/2.0/UDP%20122.172.98.23%3A50456%3Brport%3D50456%3Bbranch%3Dz9hG4bKPj97c28beead2c4175971cb07228db69f9\nvariable_sip_full_from: %3Csip%3A1000%403.110.42.145%3E%3Btag%3Dd87f0933f993479f824c7387e619bbf4\nvariable_sip_full_to: %3Csip%3A9999%403.110.42.145%3E\nvariable_sip_allow: PRACK,%20INVITE,%20ACK,%20BYE,%20CANCEL,%20UPDATE,%20INFO,%20SUBSCRIBE,%20NOTIFY,%20REFER,%20MESSAGE,%20OPTIONS\nvariable_sip_req_user: 9999\nvariable_sip_req_uri: 9999%403.110.42.145\nvariable_sip_req_host: 3.110.42.145\nvariable_sip_to_user: 9999\nvariable_sip_to_uri: 9999%403.110.42.145\nvariable_sip_to_host: 3.110.42.145\nvariable_sip_contact_params: ob\nvariable_sip_contact_user: 1000\nvariable_sip_contact_port: 50456\nvariable_sip_contact_uri: 1000%40122.172.98.23%3A50456\nvariable_sip_contact_host: 122.172.98.23\nvariable_rtp_use_codec_string: OPUS,G722,PCMU,PCMA,H264,VP8\nvariable_sip_user_agent: MicroSIP/3.21.3\nvariable_sip_via_host: 122.172.98.23\nvariable_sip_via_port: 50456\nvariable_sip_via_rport: 50456\nvariable_max_forwards: 70\nvariable_presence_id: 1000%403.110.42.145\nvariable_switch_r_sdp: v%3D0%0D%0Ao%3D-%203904558258%203904558258%20IN%20IP4%20122.172.98.23%0D%0As%3Dpjmedia%0D%0Ab%3DAS%3A84%0D%0At%3D0%200%0D%0Aa%3DX-nat%3A0%0D%0Am%3Daudio%204022%20RTP/AVP%208%200%20101%0D%0Ac%3DIN%20IP4%20122.172.98.23%0D%0Ab%3DTIAS%3A64000%0D%0Aa%3Drtpmap%3A8%20PCMA/8000%0D%0Aa%3Drtpmap%3A0%20PCMU/8000%0D%0Aa%3Drtpmap%3A101%20telephone-event/8000%0D%0Aa%3Dfmtp%3A101%200-16%0D%0Aa%3Drtcp%3A4023%20IN%20IP4%20192.168.1.18%0D%0Aa%3Dssrc%3A1025981160%20cname%3A21a156dd10a05b64%0D%0A\nvariable_ep_codec_string: CORE_PCM_MODULE.PCMA%408000h%4020i%4064000b,CORE_PCM_MODULE.PCMU%408000h%4020i%4064000b\nvariable_endpoint_disposition: DELAYED%20NEGOTIATION\nvariable_DP_MATCH: ARRAY%3A%3ADELAYED%20NEGOTIATION%7C%3ADELAYED%20NEGOTIATION\nvariable_call_uuid: c8130d8b-2109-47eb-bdbc-a8f7159f2821\nvariable_RFC2822_DATE: Sun,%2024%20Sep%202023%2010%3A00%3A58%20%2B0000\nvariable_export_vars: RFC2822_DATE\nvariable_current_application_data: 127.0.0.1%3A8085%20async%20full\nvariable_current_application: socket\nvariable_socket_host: 127.0.0.1\nContent-Type: command/reply\nReply-Text: %2BOK%0A\nSocket-Mode: async\nControl: full\n\n"; + let (input, _) = parse_any_freeswitch_event(input).unwrap(); + assert_eq!("", input); + } +} diff --git a/tests/inbound.rs b/tests/inbound.rs index b7311ee..0996c43 100644 --- a/tests/inbound.rs +++ b/tests/inbound.rs @@ -1,18 +1,187 @@ -use std::net::SocketAddr; +use std::{env, net::SocketAddr}; use ntest::timeout; use regex::Regex; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, - net::TcpListener, + net::{TcpListener, TcpStream}, task::JoinHandle, }; use anyhow::Result; -use freeswitch_esl::{Esl, EslError}; +use freeswitch_esl::CommandAndApiReplyBody; +use freeswitch_esl::{Code, Esl, EslError}; -async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { +#[tokio::test] +#[timeout(10000)] +async fn reloadxml_with_api() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let response = inbound.api("reloadxml").await; + assert_eq!(Ok("[Success]".into()), response); + Ok(()) +} +#[tokio::test] +#[timeout(5000)] +async fn reloadxml_with_bgapi() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let response = inbound.bgapi("reloadxml").await; + assert_eq!(Ok("[Success]".into()), response); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn call_user_that_doesnt_exists() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let response = inbound + .api("originate user/some_user_that_doesnt_exists karan") + .await + .unwrap_err(); + assert_eq!(EslError::ApiError("SUBSCRIBER_ABSENT".into()), response); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn send_recv_test() -> Result<()> { + use std::collections::HashMap; + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let response = inbound.send_recv(b"api reloadxml").await?; + assert_eq!( + response, + CommandAndApiReplyBody { + headers: HashMap::default(), + code: Code::Ok, + reply_text: "[Success]".into(), + job_uuid: None + } + ); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn wrong_password() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let result = Esl::inbound(stream, "ClueCons").await; + assert_eq!(EslError::AuthFailed, result.unwrap_err()); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn multiple_actions() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let body = inbound.bgapi("reloadxml").await; + assert_eq!(Ok("[Success]".into()), body); + let body = inbound + .bgapi("originate user/some_user_that_doesnt_exists karan") + .await; + assert_eq!( + Err(EslError::ApiError("SUBSCRIBER_ABSENT".to_string())), + body + ); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn concurrent_api() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let response1 = inbound.api("reloadxml"); + let response2 = inbound.api("originate user/some_user_that_doesnt_exists karan"); + let response3 = inbound.api("reloadxml"); + let (response1, response2, response3) = tokio::join!(response1, response2, response3); + assert_eq!(Ok("[Success]".into()), response1); + assert_eq!( + Err(EslError::ApiError("SUBSCRIBER_ABSENT".into())), + response2 + ); + assert_eq!(Ok("[Success]".into()), response3); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn concurrent_bgapi() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let response1 = inbound.bgapi("reloadxml"); + let response2 = inbound.bgapi("originate user/some_user_that_doesnt_exists karan"); + let response3 = inbound.bgapi("reloadxml"); + let (response1, response2, response3) = tokio::join!(response1, response2, response3); + assert_eq!(Ok("[Success]".to_string()), response1); + assert_eq!( + Err(EslError::ApiError("SUBSCRIBER_ABSENT".to_string())), + response2 + ); + assert_eq!(Ok("[Success]".to_string()), response3); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn connected_status() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + assert!(inbound.connected()); + Ok(()) +} + +#[tokio::test] +#[timeout(10000)] +async fn restart_external_profile() -> Result<()> { + let (_, addr) = get_server_address().await?; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, "ClueCon").await?; + let body = inbound.api("sofia profile external restart").await; + assert_eq!( + Ok("Reload XML [Success]\nrestarting: external".into()), + body + ); + Ok(()) +} + +#[tokio::test] +#[timeout(30000)] +async fn uuid_kill() -> Result<()> { + let (_, addr) = get_server_address().await?; + let password = "ClueCon"; + let stream = TcpStream::connect(addr).await?; + let inbound = Esl::inbound(stream, password).await?; + + let uuid = inbound + .api("originate {origination_uuid=karan}loopback/1000 &conference(karan)") + .await?; + assert_eq!("karan", uuid); + let uuid_kill_response = inbound.api("uuid_kill karan").await?; + assert_eq!("", uuid_kill_response); + Ok(()) +} + +async fn get_server_address() -> Result<(JoinHandle<()>, SocketAddr)> { let listener = TcpListener::bind("localhost:0").await?; + if let Ok(value) = env::var("INTEGRATION") { + if value.parse::().unwrap_or_default() { + let handle = tokio::spawn(async {}); + return Ok((handle, "127.0.0.1:8021".parse().unwrap())); + } + } let local_address = listener.local_addr()?; let server = tokio::spawn(async move { loop { @@ -31,7 +200,7 @@ async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { }; received_data.extend_from_slice(&buffer[0..n]); // Check for two newline characters in the received data - if let Some(index) = received_data + while let Some(index) = received_data .windows(2) .position(|window| window == b"\n\n") { @@ -60,15 +229,14 @@ async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { let reloadxml_app = format!("bgapi reloadxml\nJob-UUID: {}", new_uuids); let some_user_that_doesnt_exists = format!("bgapi originate user/some_user_that_doesnt_exists karan\nJob-UUID: {}",new_uuids); + let first_1 = "Content-Type: command/reply\nReply-Text: +OK Job-UUID: UUID_PLACEHOLDER\nJob-UUID: UUID_PLACEHOLDER\n\n"; if data_string == reloadxml_app { - let first_1 = "Content-Type: command/reply\nReply-Text: +OK Job-UUID: UUID_PLACEHOLDER\nJob-UUID: UUID_PLACEHOLDER\n\n"; - let second_1 = "Content-Length: 615\nContent-Type: text/event-json\n\n{\"Event-Name\":\"BACKGROUND_JOB\",\"Core-UUID\":\"bd0e8916-6a60-4e11-8978-db8580b440a6\",\"FreeSWITCH-Hostname\":\"ip-172-31-32-63\",\"FreeSWITCH-Switchname\":\"ip-172-31-32-63\",\"FreeSWITCH-IPv4\":\"172.31.32.63\",\"FreeSWITCH-IPv6\":\"::1\",\"Event-Date-Local\":\"2023-09-12 04:31:37\",\"Event-Date-GMT\":\"Tue, 12 Sep 2023 04:31:37 GMT\",\"Event-Date-Timestamp\":\"1694493097638660\",\"Event-Calling-File\":\"mod_event_socket.c\",\"Event-Calling-Function\":\"api_exec\",\"Event-Calling-Line-Number\":\"1572\",\"Event-Sequence\":\"18546\",\"Job-UUID\":\"UUID_PLACEHOLDER\",\"Job-Command\":\"reloadxml\",\"Content-Length\":\"14\",\"_body\":\"+OK [Success]\\n\"}"; + let second_1 = "Content-Length: 575\nContent-Type: text/event-plain\n\nEvent-Name: BACKGROUND_JOB\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2005%3A48%3A28\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2005%3A48%3A28%20GMT\nEvent-Date-Timestamp: 1695534508726403\nEvent-Calling-File: mod_event_socket.c\nEvent-Calling-Function: api_exec\nEvent-Calling-Line-Number: 1572\nEvent-Sequence: 1041\nJob-UUID: UUID_PLACEHOLDER\nJob-Command: reloadxml\nContent-Length: 14\n\n+OK [Success]\n"; let first = first_1.replace("UUID_PLACEHOLDER", &uuid_old); let second = second_1.replace("UUID_PLACEHOLDER", &uuid_old); vec![first, second] } else if data_string == some_user_that_doesnt_exists { - let first_1 = "Content-Type: command/reply\nReply-Text: +OK Job-UUID: UUID_PLACEHOLDER\nJob-UUID: UUID_PLACEHOLDER\n\n"; - let second_1 = "Content-Length: 684\nContent-Type: text/event-json\n\n{\"Event-Name\":\"BACKGROUND_JOB\",\"Core-UUID\":\"bd0e8916-6a60-4e11-8978-db8580b440a6\",\"FreeSWITCH-Hostname\":\"ip-172-31-32-63\",\"FreeSWITCH-Switchname\":\"ip-172-31-32-63\",\"FreeSWITCH-IPv4\":\"172.31.32.63\",\"FreeSWITCH-IPv6\":\"::1\",\"Event-Date-Local\":\"2023-09-13 06:56:24\",\"Event-Date-GMT\":\"Wed, 13 Sep 2023 06:56:24 GMT\",\"Event-Date-Timestamp\":\"1694588184538697\",\"Event-Calling-File\":\"mod_event_socket.c\",\"Event-Calling-Function\":\"api_exec\",\"Event-Calling-Line-Number\":\"1572\",\"Event-Sequence\":\"29999\",\"Job-UUID\":\"UUID_PLACEHOLDER\",\"Job-Command\":\"originate\",\"Job-Command-Arg\":\"user/some_user_that_doesnt_exists karan\",\"Content-Length\":\"23\",\"_body\":\"-ERR SUBSCRIBER_ABSENT\\n\"}"; + let second_1 = "Content-Length: 643\nContent-Type: text/event-plain\n\nEvent-Name: BACKGROUND_JOB\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2009%3A21%3A50\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2009%3A21%3A50%20GMT\nEvent-Date-Timestamp: 1695547310806421\nEvent-Calling-File: mod_event_socket.c\nEvent-Calling-Function: api_exec\nEvent-Calling-Line-Number: 1572\nEvent-Sequence: 6150\nJob-UUID: UUID_PLACEHOLDER\nJob-Command: originate\nJob-Command-Arg: user/some_user_that_doesnt_exists%20karan\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n"; let first = first_1.replace("UUID_PLACEHOLDER", &uuid_old); let second = second_1.replace("UUID_PLACEHOLDER", &uuid_old); vec![first, second] @@ -86,7 +254,7 @@ async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { "Content-Type: command/reply\nReply-Text: -ERR invalid\n\n" } "api reloadxml" => { - "Content-Type: api/response\nContent-Length: 14\n\n+OK [Success]\n\n" + "Content-Type: api/response\nContent-Length: 14\n\n+OK [Success]\n" } "api sofia profile external restart" => { "Content-Type: api/response\nContent-Length: 41\n\nReload XML [Success]\nrestarting: external" @@ -97,14 +265,14 @@ async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { "api uuid_kill karan" => { "Content-Type: api/response\nContent-Length: 4\n\n+OK\n" } - "event json BACKGROUND_JOB CHANNEL_EXECUTE_COMPLETE"=>{ - "Content-Type: command/reply\nReply-Text: +OK event listener enabled json\n\n" + "event plain BACKGROUND_JOB CHANNEL_EXECUTE_COMPLETE"=>{ + "Content-Type: command/reply\nReply-Text: +OK event listener enabled plain\n\n" } "api originate user/some_user_that_doesnt_exists karan"=>{ - "Content-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n\n" + "Content-Type: api/response\nContent-Length: 23\n\n-ERR SUBSCRIBER_ABSENT\n" }, "bgapi reloadxml"=>{ - "Content-Type: command/reply\nReply-Text: +OK Job-UUID: 14f61274-6487-4b79-b97b-ee0feca07e86\nJob-UUID: 14f61274-6487-4b79-b97b-ee0feca07e86\n\nContent-Length: 615\nContent-Type: text/event-json\n\n{\"Event-Name\":\"BACKGROUND_JOB\",\"Core-UUID\":\"bd0e8916-6a60-4e11-8978-db8580b440a6\",\"FreeSWITCH-Hostname\":\"ip-172-31-32-63\",\"FreeSWITCH-Switchname\":\"ip-172-31-32-63\",\"FreeSWITCH-IPv4\":\"172.31.32.63\",\"FreeSWITCH-IPv6\":\"::1\",\"Event-Date-Local\":\"2023-09-13 06:34:46\",\"Event-Date-GMT\":\"Wed, 13 Sep 2023 06:34:46 GMT\",\"Event-Date-Timestamp\":\"1694586886798662\",\"Event-Calling-File\":\"mod_event_socket.c\",\"Event-Calling-Function\":\"api_exec\",\"Event-Calling-Line-Number\":\"1572\",\"Event-Sequence\":\"29837\",\"Job-UUID\":\"14f61274-6487-4b79-b97b-ee0feca07e86\",\"Job-Command\":\"reloadxml\",\"Content-Length\":\"14\",\"_body\":\"+OK [Success]\\n\"}" + "Content-Length: 575\nContent-Type: text/event-plain\n\nEvent-Name: BACKGROUND_JOB\nCore-UUID: 0cb916f9-98ad-4fce-bcd5-5fe03c745316\nFreeSWITCH-Hostname: ip-172-31-5-95\nFreeSWITCH-Switchname: ip-172-31-5-95\nFreeSWITCH-IPv4: 172.31.5.95\nFreeSWITCH-IPv6: %3A%3A1\nEvent-Date-Local: 2023-09-24%2005%3A48%3A28\nEvent-Date-GMT: Sun,%2024%20Sep%202023%2005%3A48%3A28%20GMT\nEvent-Date-Timestamp: 1695534508726403\nEvent-Calling-File: mod_event_socket.c\nEvent-Calling-Function: api_exec\nEvent-Calling-Line-Number: 1572\nEvent-Sequence: 1041\nJob-UUID: dcab6b81-ec71-4552-b897-88721870fe16\nJob-Command: reloadxml\nContent-Length: 14\n\n+OK [Success]\n" }, _ => { "Content-Type: command/reply\nReply-Text: -ERR command not found\n\n" @@ -114,6 +282,7 @@ async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { }; let response_text = response_text.iter(); for response in response_text { + println!("writing response {:?}", response); if socket.write_all(response.as_bytes()).await.is_err() { eprintln!("error writing data"); break; // Error writing data @@ -129,148 +298,3 @@ async fn mock_test_server() -> Result<(JoinHandle<()>, SocketAddr)> { }); Ok((server, local_address)) } -#[tokio::test] -#[timeout(1000)] -async fn reloadxml() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let response = inbound.api("reloadxml").await; - assert_eq!(Ok("[Success]".into()), response); - Ok(()) -} -#[tokio::test] -#[timeout(30000)] -async fn reloadxml_with_bgapi() -> Result<()> { - let (_, addr) = mock_test_server().await?; - // let addr = "localhost:8091"; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let response = inbound.bgapi("reloadxml").await; - assert_eq!(Ok("[Success]".into()), response); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn call_user_that_doesnt_exists() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let response = inbound - .api("originate user/some_user_that_doesnt_exists karan") - .await - .unwrap_err(); - assert_eq!(EslError::ApiError("SUBSCRIBER_ABSENT".into()), response); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn send_recv_test() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let response = inbound.send_recv(b"api reloadxml").await?; - let body = response.body().clone().unwrap(); - assert_eq!("+OK [Success]\n", body); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn wrong_password() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let result = Esl::inbound(addr, "ClueCons").await; - assert_eq!(EslError::AuthFailed, result.unwrap_err()); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn multiple_actions() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let body = inbound.bgapi("reloadxml").await; - assert_eq!(Ok("[Success]".into()), body); - let body = inbound - .bgapi("originate user/some_user_that_doesnt_exists karan") - .await; - assert_eq!( - Err(EslError::ApiError("SUBSCRIBER_ABSENT".to_string())), - body - ); - Ok(()) -} - -#[ignore] -#[tokio::test] -#[timeout(30000)] -async fn concurrent_api() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let response1 = inbound.api("reloadxml"); - let response2 = inbound.api("originate user/some_user_that_doesnt_exists karan"); - let response3 = inbound.api("reloadxml"); - let (response1, response2, response3) = tokio::join!(response1, response2, response3); - assert_eq!(Ok("[Success]".into()), response1); - assert_eq!( - Err(EslError::ApiError("SUBSCRIBER_ABSENT".into())), - response2 - ); - assert_eq!(Ok("[Success]".into()), response3); - Ok(()) -} - -#[ignore] -#[tokio::test] -#[timeout(30000)] -async fn concurrent_bgapi() -> core::result::Result<(), EslError> { - let addr = "localhost:8021"; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let response1 = inbound.bgapi("reloadxml"); - let response2 = inbound.bgapi("originate user/some_user_that_doesnt_exists karan"); - let response3 = inbound.bgapi("reloadxml"); - let (response1, response2, response3) = tokio::join!(response1, response2, response3); - assert_eq!(Ok("[Success]".to_string()), response1); - assert_eq!( - Err(EslError::ApiError("SUBSCRIBER_ABSENT".to_string())), - response2 - ); - assert_eq!(Ok("[Success]".to_string()), response3); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn connected_status() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - assert!(inbound.connected()); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn restart_external_profile() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let inbound = Esl::inbound(addr, "ClueCon").await?; - let body = inbound.api("sofia profile external restart").await; - assert_eq!( - Ok("Reload XML [Success]\nrestarting: external".into()), - body - ); - Ok(()) -} - -#[tokio::test] -#[timeout(30000)] -async fn uuid_kill() -> Result<()> { - let (_, addr) = mock_test_server().await?; - let password = "ClueCon"; - let inbound = Esl::inbound(addr, password).await?; - - let uuid = inbound - .api("originate {origination_uuid=karan}loopback/1000 &conference(karan)") - .await?; - assert_eq!("karan", uuid); - let uuid_kill_response = inbound.api(&format!("uuid_kill karan")).await?; - assert_eq!("", uuid_kill_response); - Ok(()) -}